# 一、开始
Vue-CLI 是 Vue 官方脚手架。,包含初始化工程、增加插件等功能。本次分析的 Vue-CLI 版本是 v4.5.11
。
# 二、Vue-CLI
Vue-CLI
库属于 monorepo
模式,除了 @vue/cli
这个 NPM 包之外,还有一些 plugin
、vue-cli-service
等。
Vue-CLI
对外暴露的 bin
命令是 vue
。全局安装 Vue-CLI
后,在终端输入 Vue
,会打印出 Vue-CLI
的帮助信息。
$ Vue
Usage: Vue <command> [options]
Options:
-V, --version output the version number
-h, --help output usage information
Commands:
create [options] <app-name> create a new project powered by vue-cli-service
add [options] <plugin> [pluginOptions] install a plugin and invoke its generator in an already created project
invoke [options] <plugin> [pluginOptions] invoke the generator of a plugin in an already created project
inspect [options] [paths...] inspect the webpack config in a project with vue-cli-service
serve [options] [entry] serve a .js or .vue file in development mode with zero config
build [options] [entry] build a .js or .vue file in production mode with zero config
ui [options] start and open the vue-cli ui
init [options] <template> <app-name> generate a project from a remote template (legacy API, requires @vue/cli-init)
config [options] [value] inspect and modify the config
outdated [options] (experimental) check for outdated vue cli service / plugins
upgrade [options] [plugin-name] (experimental) upgrade vue cli service / plugins
migrate [options] [plugin-name] (experimental) run migrator for an already-installed cli plugin
info print debugging information about your environment
Run vue <command> --help for detailed usage of given command.
从上面可以看到 Vue-CLI
中可以使用的命令包括 vue create
、vue add
、vue invoke
等。
在源码的 packages/@vue/cli/bin/vue.js (opens new window) 文件可以看到所有命令。
Vue create
是 Vue-CLI
最重要的内容,下面重点看下 Vue create
的流程,以及值得我们学习的地方。
# 三、Vue create
Vue create
作用是创建一个 Vue
项目,该命令提供了丰富的选项,比如可选择 Babel 版本、是否使用 Typescirpt/Eslint/单元测试/e2e测试
等。
# 1. 总体流程
直接看图:
# 2. 注入 promptModules
Vue-CLI
中 prompt
的生成利用了依赖注入的思想,就是把高层次(这里是 Creator)依赖的模块(这里是 prompt)通过传参的方式注入到高层次模块内部。
// create.js
const { getPromptModules } = require('./util/createTools')
async function create (projectName, options) {
// ...
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
// Creator.js
module.exports = class Creator extends EventEmitter {
constructor (name, context, promptModules) {
// ...
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
}
}
// PromptModuleAPI.js
module.exports = class PromptModuleAPI {
constructor (creator) {
this.creator = creator
}
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
getPromptModules
是获取了所有要注入的 prompt
,不同类型的 prompt
在单独的文件中。
// util/createTools.js
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
比如 vueVersion.js
作用是选择 vue
版本:
// lib/promptModules/vueVersion.js
module.exports = cli => {
cli.injectFeature({
name: 'Choose Vue version',
value: 'vueVersion',
description: 'Choose a version of Vue.js that you want to start the project with',
checked: true
})
cli.injectPrompt({
name: 'vueVersion',
when: answers => answers.features.includes('vueVersion'),
message: 'Choose a version of Vue.js that you want to start the project with',
type: 'list',
choices: [
{
name: '2.x',
value: '2'
},
{
name: '3.x (Preview)',
value: '3'
}
],
default: '2'
})
cli.onPromptComplete((answers, options) => {
if (answers.vueVersion) {
options.vueVersion = answers.vueVersion
}
})
}
promptModules
为 cli 注入 featurePrompt
、injectedPrompts
,featurePrompt
是外层的 feature
,比如Bable
、Typescript
、Router
、Vuex
这些选项,injectedPrompts
是选择了某个 feature
后,再次弹出的选择,比如选择 Vue 版本。
featurePrompt
举例:
$ vue create test-vue-cli
Vue CLI v4.5.11
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to toggle all, <i> to invert selection)
❯◉ Choose Vue version
◉ Babel
◯ TypeScript
◯ Progressive Web App (PWA) Support
◯ Router
◯ Vuex
◯ CSS Pre-processors
◉ Linter / Formatter
◯ Unit Testing
◯ E2E Testing
injectedPrompts
举例:
? Please pick a preset: Manually select features
? Check the features needed for your project: Choose Vue version, Babel, Linter
? Choose a version of Vue.js that you want to start the project with (Use arrow keys)
❯ 2.x
3.x (Preview)
# 3. 插件系统
Vue-CLI 3
是基于插件 (opens new window)的,插件可以:
- 修改项目的
webpack
配置 - 添加新的
vue-cli-service
命令 - 扩展
package.json
- 在项目中创建新文件、或者修改老文件
- 提示用户选择一个特定的选项
插件的实现原理是,每个插件提供一个函数,接受 api
、options
、root options
三个参数,api
参数由 Vue-CLI
提供,在 GeneratorAPI.js (opens new window) 中,主要有下面几种:
hasPlugin
:判断项目中是否有某个插件extendPackage
:拓展package.json
配置render
:利用ejs
渲染模板文件onCreateComplete
:内存中保存的文件字符串全部被写入文件后的回调函数exitLog
:当generator
退出的时候输出的信息genJSConfig
:将json
文件生成为js
配置文件injectImports
:向文件当中注入import
语法的方法injectRootOptions
:向 Vue 根实例中添加选项
插件执行的时机是在 generator.generate()
中,可对照着上面的总流程图看。
// Generator.js
module.exports = class Generator {
async generate ({
extractConfigFiles = false,
checkExisting = false
} = {}) {
await this.initPlugins()
// ...
}
async initPlugins () {
const { rootOptions, invoking } = this
const pluginIds = this.plugins.map(p => p.id)
// apply hooks from all plugins
for (const id of this.allPluginIds) {
const api = new GeneratorAPI(id, this, {}, rootOptions)
const pluginGenerator = loadModule(`${id}/generator`, this.context)
if (pluginGenerator && pluginGenerator.hooks) {
await pluginGenerator.hooks(api, {}, rootOptions, pluginIds)
}
}
// ...
// apply generators from plugins
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions, invoking)
if (apply.hooks) {
// while we execute the entire `hooks` function,
// only the `afterInvoke` hook is respected
// because `afterAnyHooks` is already determined by the `allPluginIds` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
// restore "any" hooks
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
}
}
}
# 4. preset
Vue-CLI
中的 preset
可以用来管理插件及其他配置,当用户 Vue create
创建项目时保存了自己的选项,就可以通过 vue config
命令查看此 preset
,其保存在 ~/.vuerc
文件中。
preset
举例:
{
"useConfigFiles": true,
"cssPreprocessor": "sass",
"plugins": {
"@vue/cli-plugin-babel": {},
"@vue/cli-plugin-eslint": {
"config": "airbnb",
"lintOn": ["save", "commit"]
},
"@vue/cli-plugin-router": {},
"@vue/cli-plugin-vuex": {}
}
}
preset
在源码的作用是也是管理插件,也就是插件是从 preset
的配置上取的。
// Creator.js
module.exports = class Creator extends EventEmitter {
async create (cliOptions = {}, preset = null) {
// ...
preset = await this.promptAndResolvePreset()
preset = cloneDeep(preset)
// inject core service
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
// ...
}
}
# 四、其他命令
Vue add
,安装插件并调用。安装插件其实就是执行了npm install/yarn install
,调用插件就是实例化一个 Genarator,然后调用generator.generate()
方法,和Vue create
的一部分流程类似。Vue invoke
,调用插件。和Vue add
不同的是没有安装插件的部分Vue inspect
,审查一个Vue Cli
项目的webpack配置。内部会调用vue-cli-service
中的inspect
方法,config
可以通过api.resolveWebpackConfig()
拿到,然后将其保存成文件即可。
# 五、create-react-app
Vue-CLI
和 create-react-app
很像,下面是使用 create-react-app
初始化一个 React
项目的流程图:
create-react-app
生成的项目可以通过 react-scripts
执行 start/build
命令,react-scripts
就相当于vue-cli-service
。
Vue CLI 的模版文件是通过 vue-cli-service
注入的,而 create-react-app
的模版是直接复制的 cra-teamplate
。