本文会从三个方面讲起:
- cli3产生的原因
- cli3中的最佳实践
- cli3的源码简析
一、cli3产生的原因
cli1.x,2.x
看一下之前的vue-cli创建项目的方式:
vue init webpack
vue-cli 的缺点也很明显:
- 从多个模板中选择一个,模板间没有交集
- 一旦创建项目,就与vue cli没什么关系了,创建的配置并不能反馈到vue cli的上游
- 没有一个可视化页面可以来管理我们的vue项目,同时项目依赖的更新也是一个难题
cli3的能力
上面简述了原有的cli的缺点,下面罗列了一些cli3的新的特点:
- 去掉了cli2原有的config和build文件夹
- 提供了一些最佳实践,默认使用这些最佳实践,比如thread-loader的使用多个处理器来处理babel或者ts的编译,默认使用cache-loader等
- 虽然有了最佳实践,但是也不一定是我们想要的。所以cli3提供了可配置化功能,即在根目录的vue.config.js,通过vueconfigjs,我们可以依然具备了操作webpack,webpack-dev-server,loader,plugin的能力。
- cli3采用了插件机制,这里的插件机制的解析我们后面会详细讲到。正是因为这个插件机制,让我们更便于扩展,可以看到,目前已有590个插件,包括我们常见的一些框架与技术,比如graphql。storybook,jest等

6. cli3提供了图形化界面和命令行两种方式,通过图形化界面,我们可以很直观的管理vue项目,启动编译测试项目,添加插件,更新安装依赖,更改部分配置等。值得一提的是,打包后也为我们分析了包的大小(webpack-bundle-analyzer),如果包较大的话,会有warning提示我们利用webpack的code-split去拆包,达到一个更好的性能。

cli3中的最佳实践
安利了一波vue-cli3之后,接下来我们来看一下,我们所说的vu-cli3为我们集成的最佳实践到底是什么呢?
resource hint(preload-wepack-plugin)



在cli3中,这个功能的实现是采用了webpack的一个插件,preload-wepack-plugin。也就是在插入script标签到index.html之前,获取chunk,并对资源的引入做了改变。
动态添加polyfill(browerslist):
默认情况下,它会把 useBuiltIns: 'usage' 传递给 @babel/preset-env,这样它会根据源代码中出现的语言特性自动检测需要的 polyfill。这确保了最终包里 polyfill 数量的最小化。

{
"presets": [
[
"@babel/preset-env",
{
"useBuiltIns": "usage"
}
]
]
}
contenthash
webpack中对于输出文件名可以有三种hash值:
hash
chunkhash
contenthash
这三者有什么区别呢?
- hash: 每次修改任何一个文件,所有文件名的hash都将改变。所以一旦修改了任何一个文件,整个项目的文件缓存都将失效
- chunkhash: 根据不同的入口文件(Entry)进行依赖文件解析、构建对应的chunk,生成对应的哈希值。将样式作为模块import到JavaScript文件中的,所以它们的chunkhash是一致的,这样就会有个问题,只要对应css或则js改变,与其关联的文件hash值也会改变,但其内容并没有改变呢,所以没有达到缓存意义。
- contenthash: 是针对文件内容级别的,只有你自己模块的内容变了,那么hash值才改变
那么,使用这种contenthash模式,再结合我们的http缓存,我们可以做到针对文件级别的缓存。如果我们的项目上线是使用一个非覆盖式发布,那这种模式是再适合不过了。
modern mode
什么是modern mode? 官网给出了一个友好的介绍


{
"presets": [
[
"@babel/preset-env",
{
"targets": {
"esmodules": true
}
}
]
]
}
实际上,vue-cli3还为我们集成了很多的best-practice,例如
- webpack的devtools在dev环境使用的cheap-module-eval-source-map,在prod环境使用的source-map
cli3源码简析
首先安利一下vue-cli3源码分析的文档。但是接下来对于源码的分析可能不会直接上代码,我更希望通过流程图,以一个简单明了的方式,和大家阐明插件机制是如何实现的.
不同于之前 1.x/2.x 的 vue-cli 工具都是基于远程模板去完成项目的初始化的工作,它属于那种大而全的方式,当你需要完成自定义的脚手架工具时,你可能要对 vue-cli 进行源码级别的改造,或者是在远程模板里面帮开发者将所有的配置文件初始化完成好。而 @vue/cli3 是基于插件机制的,它将原来的大而全的模板拆解为现在基于插件系统的工作方式,每个插件只需要完成自己对于项目的拓展工作
vue-cli3主要由cli和cliservice组成。
整个插件系统当中包含2个重要的组成部分:
- @vue/cli,提供 cli 命令服务,例如vue create创建一个新的项目;
- @vue/cli-service,提供了本地开发构建服务
1. cli
cli为我们提供了11个命令,这里重点会讲到vue create和vue add 命令

vue create
vue create是用来创建项目的,看一下vue create命令做了什么事情

- 首先会对项目名进行验证,使用的是validate-npm-package-name 这个包来验证。然后会判断项目名在当前目录中是否已经存在,并提供了三种方式来进行操作,分别是 重写,合并,取消。
- 接下来是获取预设,什么是预设呢,引用官方文档上的话,预设就是
Vue CLI 预设配置是一个包含创建新项目所需的预定义选项和插件的 JSON 对象,让用户无需在命令提示中选择它们。
也就是说,我们上一次创建的项目配置,可以保留在一个文件(也就是vuerc)中,下一次创建时,就可以使用这些配置,而不用重新在命令行中再次操作,再去选择一次。
这第二步就是获取我们需要的配置。
使用到了inquirer,inquirer.js是一个nodejs模块,是用户与命令行交互的工具
3. 接下来是依赖安装。
在上一步中我们已经知道了需要的插件,cli帮我们生成pkg.json,并通过yarn或者npm安装依赖,这里使用了promise.race,通过获取同个npm包来看看是默认镜像还是淘宝镜像返回更快,那个更快就使用哪个。
4. 最后一步就是generator,也就是生成我们的文件,又分别做了以下的操作.
- i. 调用各个插件,各个插件都可以对我们的项目进行修改
- ii. 修改完了之后,合并配置(因为各个插件都拥有改变文件的能力,所以需要将改动合并起来)
- iii. 先将整个项目的文件在内存中生成好
- iv. 生成文件
vue add
以上讲的是关于vue create的,接下来我们看一下vue add命令,add命令是用来添加插件的。通过add的命令,cli3会帮我们完成插件的下载,安装以及执行插件所提供的 generator,我们来看一下流程图

- 安装插件和依赖
- 加载generator: 可以来修改pkg.json中的字段,或者是基于ejs模板来修改文件,或者是生成一个新的文件
- 加载并调用Prompts:这是一个对话模块,底层使用 inquirer 进行展示。如果这个插件在其根目录包含一个 prompts.js,那么它将会用在该插件被初始化调用的时候。Inquirer.js会帮我们解析 这些被解析的答案对象会作为选项被传递给插件的 generator
- run generator: 使用上一步的选项来生成,修改文件
2. cli-service
接下来我们来看一下@vue/cli-service 内部是如何搭建整个插件系统的。就拿执行npm run serve启动本地开发服务来说,大概流程是这样的:

- 实例化 Service类
- 在实例化 Service的过程当中完成了两个比较重要的工作:
- 加载插件(resolvePlugin:加载@vue/cli-service 内部提供的插件以及项目应用当中需要使用的插件,内部插件又分为两类,一类插件在内部动态注册新的 CLI 命令,开发者即可通过 npm script 的形式去启动对应的 CLI 命令服务,一类插件主要是完成 webpack 本地编译构建时的各种相关的配置。cli-service 将 webpack 的开发构建功能收敛到内部来完成)
- Service 实例化完成后,调用实例上的 run方法来执行所有被加载的插件
总结: cli-service 将基于 webpack 的本地开发构建配置收敛至内部来实现,当你没有特殊的开发构建需求的时候,内部配置可以开箱即用,不用开发者去关心一些细节。当然在实际团队开发当中,内部配置肯定是无法满足的,得益于插件构建设计,开发者不需要重构内部的配置,而是直接使用 @vue/cli-service 暴露出来的 API 去完成对于特殊的开发构建需求。
最后
- 举个插件的栗子: 传送门: vue-cli-plugin-element 尽管我觉得vue-cli-plugin-element可以做得更好,比如自定义选择需要添加的组件,自定义主题色(vue ui的交互页面其实是提供了颜色选择器的,如下图),模板选择等

插件(Plugin)模式向用户提供了一种扩展程序的接口,用户可以在程序本体之外,按照指定接口编写插件来为程序增加功能