沐光

记录在前端之路的点点滴滴

使用 vue-cli 发布一个完善的包

前言

其实先前有写过一篇《发布一个 vue 包》,不过对于“极简主义者”来说,该文章还是显得有些复杂,毕竟需要配置“复杂”的 webpack。那么有没有一种简单上手、开箱即用的方法呢?答案是有的,vue-cli 已经完美的考虑到了这些问题,此处就介绍如何使用 vue-cli 来搭建一个完整的发包环境。

注:此处的 vue-cli 指的是 @vue/cli ,为 3.x 版本。

目标

我们的最终目标是发布的包、测试环境和包的使用文档,此可概括为:项目构建环境和项目使用文档。由于 vue-cli 可以手动配置项目需要的环境,因此在项目构建环境部分此处仅记录 vue-cli 的部分配置选项和相关解释,整体的项目结构和先前的基本一致。

然而文档方面因为 vue-cli 没有做相应的支持,因此我们需要找一个能识别 vue 组件的文档库,个人建议首选 vuepress,其次可以尝试 docsify 或者其余能够支持 vue 的文档网站生成工具(当然自己造对应轮子也是可以的)。

项目环境构建

使用 vue-cli 创建一个项目,此处就以 vue-cli-plugin-demo 做为例子。直接使用 vue create 创建项目,然后选择 Manully select features,之后选择的内容如下图:

vue-cli 配置

这里解释一下其中部分选项的选择的原因:

TS 的 Class 风格支持

因为 vue3 相对于 vue2 版本有比较大的变动,至于 vue2 所支持的 class 风格的 TS 写法在 vue3 中可能不怎么支持,因此现有项目是否使用 Class 风格可能会有些争议(主要是担心写习惯后到时候不太好切换写法)。

个人建议使用,毕竟单纯的 TS 写法在 vue2 中的会稍显难受,装饰器的写法要稍微舒服一些。此外,多学习一些新知识总归是对自己有好处。

TS 使用 eslint-standard

有人会问为什么创建项目时不直接选 TSlint 作为校验规则呢?这其实也是有原因的。可以上网对比一下 ESlint 和 TSlint ,你会发现 ESlint 的校验规则相对来说更细致,且更完全。此外,TypeScript 官方都决定迁移 TSlint 至 typescript-eslint,那我们还有什么理由坚持 TSlint 了呢?嘿嘿~

The TypeScript Team themselves also announced their plans to move the TypeScript codebase from TSLint to typescript-eslint, and they have been big supporters of this project. More details at https://github.com/microsoft/TypeScript/issues/30553

其余部分

单元测试部分因为我仅仅了解 Mocha 的部分内容,因此选择的是 Mocha,熟悉 Jest 的可以选 Jest;至于勾选 vue-router 和 vuex,这个是对于运行 devServer 做的一些配置;然后是 vue-router 不选择 history 模式,这个仁者见仁,智者见智吧,毕竟 lib 发包不走这个。

项目配置

结构配置

过滤掉默认生成的并且不做修改的部分,最终项目的结构大致为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
vue-cli-plugin-demo
├── components.json
├── docs
├── packages
├── src
| ├── App.vue
| ├── components
| ├── entry.js
| ├── main.js
| ├── mixins
| ├── router.js
| ├── store.js
| ├── views
| └── types
├── types
| └── index.d.ts
├── vue.config.js
├── tsconfig.json
├── webpack.component.js
└── yarn.lock

其中 docs 为文档的文件夹,然后 components.json 、webpack.component.js 和 src/entry.js 主要为了按需引入而做的单独打包处理,其次在根目录和 src 文件夹内都添加了一个 types 文件,最后在根目录下建立了一个 packages 文件夹作为发包组件的结构。接下来我们一步步来搭建我们自己的 vue 包。

整体结构参考 element-ui 的项目结构

配置 tsconfig.json 和 vue.config.js

tsconfig.json 内的配置内容不是很多,仅配置以下信息:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
"jsxFactory": "h", // 简写 jsx 的 render 函数
"paths": [ // 配置 import 引入的路径索引,视情况扩展
// ...,
"@views/*": [
"src/views/*"
]
],
"pretty": true, // 为错误消息设置样式
"includes": [ // 添加需要 ts 校验的文件
// ...
"packages/**/*.ts",
"packages/**/*.tsx",
"packages/**/*.vue"
]

vue.config.js 内的配置如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const path = require('path')

// 配置公共请求路径
function resolve (dir) {
return path.join(__dirname, dir)
}

module.exports = {
lintOnSave: true,
productionSourceMap: false,
chainWebpack: config => {
// 设置 output 的默认 export 值
config.output.libraryExport('default')
// 新增部分别名,视情况扩展
config.resolve.alias
.set('@', resolve('src'))
.set('@views', resolve('src/views'))
// ...
// 新增 js 的 exclude 内容
config.module.rule('js').exclude.add([resolve('lib'), resolve('dist')])
}
}

注: 两文件内的别名扩展需要保持一致。除了配置别名外,另一个需要关注的部分是修改 rule(‘js’) 部分的配置,否则后面测试自己打包好的 lib 文件会报错。

packages 包写法

packages 包用于存放我们编写的包组件,我们统一其每一项的结构为:

1
2
3
4
5
/*...*/vue-cli-project-demo/packages
└── button
├── index.ts
└── src
└── main.vue

这么写的原因在于方便按需打包,使用统一的格式利于我们抽象对应的函数方法,同时也利于项目的维护。其中 index.ts 用于暴露 src 中的 vue 文件和其对应的 install 扩展方法。

index.ts 内没有必要对单文件进行 install 扩展,除非需要规范化对应 component 的标签值,比如:element-ui 单文件引入的是 Button,没有 el 前缀,用 Vue.use 就是为了补充上前缀,内部走 Vue.component 方法。

个人建议:保证导出的组件的 name 值符合总体规范即可,并不需要一定走此配置的格式。

打包配置

这里使用的是 vue-cli 提供的 lib 打包方式,基本不用额外的 webpack 配置,命令为:

1
vue-cli-service build --target lib --name myLib [entry]

因此我们可以改造 package.json 如下:

1
2
3
4
5
6
7
8
"scripts": {
"dev": "vue-cli-service serve --open",
"build": "vue-cli-service build --target lib --dest lib ./src/entry.ts && yarn removeHtml",
"removeHtml": "rm ./lib/*.html",
"lint:js": "vue-cli-service lint",
"lint:css": "npx stylelint src packages --fix",
"test:unit": "vue-cli-service test:unit"
}

因为构建 lib 包会默认生成一个 html 文件,在此我做了删除。此外添加了 stylelint 的一些配置。demo 内还规范了发包上传的内容以及 commit 时的校验配置,可视情况添加。

此外如果更规范一点,我们可以写上作者,写作者等配置(可参考 demo)。

dev 环境配置

开发环境需要配置的东西基本没有,这里仅仅介绍一下如何引用打包文件。此处有两种情况:

在开发包的阶段,我们直接引用 entry.ts 文件即可,当测试生成的包时,我们引用 lib 内的生成的文件。如:

1
2
3
4
// 开发时测试用
import MyComponents from './entry';
// 构建完测试用
import MyComponents from '@lib/vue-cli-project-demo.umd';

此时我们需要配置对应 js 文件的 module 扩展,如 demo 内的 global.d.ts 的声明:

1
2
3
4
5
declare module "@lib/*" {
import Vue, { PluginFunction } from 'vue';
const _default: PluginFunction<Vue>;
export default _default;
}

项目开发相关的 ts 问题可查阅先前的一篇 ts 问题总结篇,配置开发包的 ts 问题看本文的问题汇总。

Test 测试配置

demo 内使用的是 Mocha + chai,同时对 slot 部分做了一下测试,这个对着文档就能做出一些测试函数,其余的配置 vue-cli 都封装好了。测试的语法并非本文的重点,有机会可以补上一篇。

单文件打包配置

要实现按需引入的话,我们还需配置多入口打包。先前规范的 package 文件夹的结构刚好就支持我们遍历需要打包的文件。

由于想稍微偷点懒,这里我就直接用 json 记录各需要的入口文件,同时配置 webpack.components.js 仅用于单独打包。webpack 的详细配置其实可以参考 vue-cli 的内部配置(vue inspect),在此我就不一一列举了,其它需要注意的部分就是如果 package 包内的组件如果用 TS 写的,那么尽量不要压缩代码,可能会出现 name 被压缩的问题了。

文档配置

对于整个项目来说,文档的配置可以算是一个点缀项,能让整体更加完善。vuepress 文档已经十分详细了,这里仅仅记录一些比较困难的配置点。docs 文件的结构大致为:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
vue-cli-project-demo/docs
├── .vuepress
| ├── config.js
| ├── enhanceApp.js
| ├── public
| | ├── favicon.ico
| | └── logo.jpg
| ├── router-config.js
| ├── static-theme-config.js
| └── utils
| └── index.js
├── components
| ├── button.md
| └── index.md
└── index.md

注: docs 才是文档页面的根路径。

图片配置

Vuepress 内分为文档图片资源和静态图片资源。公共图片资源就不用介绍了,一般相对路径的 markdown 写法能完全满足要求。但是对于页面的 favicon 和首页模板部分的 logo ,这个需要在 .vuepress 下的 public 文件内做存储。

项目包引入

对于我们生成的 lib 包的引入,此需要在 .vuepress 文件夹下创建一个 enhanceApp.js 文件,在该文件内做额外包的引入。写法为:

1
2
3
4
5
6
7
8
9
10
11
// 安装发布的包
import MyComponents from '../../lib/vue-cli-project-demo.umd';

export default ({
Vue, // VuePress 正在使用的 Vue 构造函数
options, // 附加到根实例的一些选项
router, // 当前应用的路由实例
siteData // 站点元数据
}) => {
Vue.use(MyComponents)
}

整体页面配置

与 .vuepress 同级别的其余文件/文件夹为对应的页面模板,例如:默认进入显示的首页为 index.md 文件,对于各文件的路径配置需要在 .vuepress/config.js 文件内对应编写。

这里可能会遇到的问题是,将 config.js 文件拆分成多个文件时,页面会无法监听到其余 js 配置文件的修改。我们此时需要配置 extraWatchFiles 字段来额外监听需要监听的文件。

另外需要注意的是 base 属性,如果部署在服务器上的路径非根路径,那么此还需要对应设置一下。剩余的配置都为比较基础的配置了,demo 内都有较为详细的说明,在此就不一一列举了。

部署

因为我的 github.io 上已经有我的博客了,此部分具体有什么坑我也没有踩过,仅能给大家抛个链接了。

Demo 地址

传送门

问题汇总

引入打包的 lib 文件报错

在测试生成的包时,通过 import 引入报错,其原因是 webpack 对于 import 的文件会进行此处的 js 解析,然而对于已经打包好的文件我们预想的是直接引入就行了,因此需要将其从规则中去除掉,否则通过规则去解析就会报错。

两个 types 文件的目的

项目内的 types 文件其实可以合并成一个,但是个人倾向分成两部分。与 src 同层的 types 文件仅用来声明 lib 默认导出的文件,而 src/types 文件则是项目开发时需要的文件声明。如果最终发布至 npm 上的包仅包含 lib 文件,那么建议使用 demo 的这种写法,层次会更清楚一点。

使用 lib 中的 umd.min.js 文件报错

由于 lib 打包生成的 min 文件将组件的 Class 名压缩了,因此对应的 name 值就不是我们设置的值,即使将 name 显示至于 Component 内也不行,对此我发了一条 issue

补充: 最终的解决方案有两个,这里列举一下解决思路:

方法 1:

1
2
3
4
5
6
7
8
// 对应包组件
@Component({
name: 'MyButton'
})
export default class MyButton extends Vue {};

// install 方法内的 component 注册部分
Vue.component(MyButton.options.name, MyButton);

方法 2:

配置 terser 的代码压缩部分,令其不压缩 className,该部分在 vue.config.js 内配置如下内容。

1
2
3
4
5
configureWebpack: config => {
if (process.env.NODE_ENV === 'production') {
config.optimization.minimizer[0].options.terserOptions.keep_fnames = true
}
}

注意: vue-cli 使用的是 terser 而不是 uglify,此外虽然 vue 内有对应 terser 的命名,但是没法获取得到,使用 config.optimization.minimizer(‘terser’) 函数会使得 minimizer 部分的内容丢失,暂未找到比较好的解决方法。个人还是希望官方补充一下解决该问题的思路。

1
2
3
4
5
6
7
8
9
10
// 源码中的 production 环境的部分配置
if (process.env.VUE_CLI_TEST) {
webpackConfig.optimization.minimize(false)
} else {
const TerserPlugin = require('terser-webpack-plugin')
const terserOptions = require('./terserOptions')
webpackConfig.optimization
.minimizer('terser')
.use(TerserPlugin, [terserOptions(options)])
}

源码请见 prod.js
官方 terserOptions 配置

install 方法报错

在对应包的 index.ts 内为导出的包设置 install 方法时报错,install 方法在 Vue 中是作为 Plugins 的一个内置函数而设置的,而我们导出的实例类型为 VueConstructor ,在 VueConstructor 上并没有 install 函数,因此按照 plugins 那种写法扩展声明一个即可。

引入包的项目构建页面不呈现

使用 Components 语法编写的组件库可能会遇到这么一个问题,项目将组件通过 Vue.use 方法全量注册到项目中了,开发环节并没有遇到任何问题,可随处使用,但是一旦 build 构建后,页面却无法呈现出组件。

原因: Class 语法编写的组件编译时会经有 vue-cli 的 webpack 处理,默认的 terser 压缩配置中同样会将 ClassName 给压缩掉,这就导致引入的组件在生产环境找不到了。

解决方法:参考 使用 lib 中的 umd.min.js 文件报错方法 2

参考文档