VueCLI Generator 源码分析
使用 Vue CLI 的 vue create 创建项目后,新项目里会有一些初始文件,这些文件是怎么来的呢?它们都是通过 Vue CLI 的 Generator 生成的。
本篇文章接下来就会介绍 Vue CLI 的 Generator 的相关知识。
Generator 和 插件
Vue CLI 使用了一套基于插件的架构,每个插件的目录结构如下:
通常,一个需要操作文件的插件会有 generator.js
或者 generator/index.js
,这两个文件就是每个插件操作项目文件的 Generator 逻辑。
Generator 和 vue create
Generator 在 vue create 的过程中会被调用,我们先看看 vue create 的基本流程。
vue create 运行过程分析
- vue create 命令在 create.js 中生成 Creator 实例
- 调用 creator.create() 开始创建
- resolvePreset 加载预设文件
- promptAndResolvePreset 交互式配置预设选项
- 初始化 package.json : 如果没有就创建默认的,已存在就更新特定的几个插件(dependence)
- npm install
- resolvePlugins:遍历插件
- generator 方法设置为插件的 apply 方法
- 创建 Generator 实例
- 调用 generator 的 generate 方法
- npm install
- 执行 Generator 的 afterInvokeCbs、afterAnyInvokeCbs 的钩子函数
- 检查 readme,若没有则生成
- 初始化 git (如果需要的话)
通过上面的流程发现,generator 的逻辑主要在上面列举的 6、7 两步。
Generator 初始化
我们需要重点关注 Genrator 实例初始化的以下变量
pkg
: package.json 内容files
: 记录 Generator 需要生成的文件 (文件目录 -> 文件文本内容 的映射)fileMiddlewares
:保存 VueCLI 插件中的api.render
生成的文件操作postProcessFilesCbs
:保存 VueCLI 插件中的api.postProcessFiles
生成的文件操作afterInvokeCbs
:保存api.afterInvoke
生成的钩子函数afterAnyInvokeCbs
:保存api.afterAnyInvoke
生成的钩子函数
// https://github1s.com/vuejs/vue-cli/blob/HEAD/packages/@vue/cli/lib/Generator.js#L105-L149
class Generator {
constructor () {
// 插件
this.plugins = sortPlugins(plugins)
// package.json
this.pkg = Object.assign({}, pkg)
// generator 钩子
this.afterInvokeCbs = afterInvokeCbs
this.afterAnyInvokeCbs = afterAnyInvokeCbs
// virtual file tree, 虚拟文件树
this.files = Object.keys(files).length
// when execute `vue add/invoke`, only created/modified files are written to disk
? watchFiles(files, this.filesModifyRecord = new Set())
// all files need to be written to disk
: files
this.fileMiddlewares = []
this.postProcessFilesCbs = []
// load all the other plugins
this.allPlugins = this.resolveAllPlugins()
}
}
Generator 的 generate 方法
initPlugins
: 遍历插件,调用 GeneratorAPI 更改初始化的插件extractConfigFiles
:根据配置把package.json
中 babel、postcss、eslintConfig、jest、browserslist、browserslist 的配置抽取为独立的配置文件resolveFiles
: 1. 遍历执行 fileMiddlewares ,修改 files 变量 2. 文件生成后,遍历执行 postProcessFilesCbs- 添加 pkg 内容到 files
writeFileTree
:保存 files 变量内的内容到硬盘
// https://github1s.com/vuejs/vue-cli/blob/HEAD/packages/@vue/cli/lib/Generator.js#L193-L211
class Generator {
async generate ({
extractConfigFiles = false,
checkExisting = false
} = {}) {
await this.initPlugins()
// save the file system before applying plugin for comparison
const initialFiles = Object.assign({}, this.files)
// extract configs from package.json into dedicated files.
this.extractConfigFiles(extractConfigFiles, checkExisting)
// wait for file resolve
await this.resolveFiles()
// set package.json
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// write/update file tree to disk
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
}
GeneratorAPI 介绍
GeneratorAPI 是暴露给每个 VueCLI 插件用来生成/删除/修改项目文件的。
api.render
:生成一个 fileMiddleware,用来把目录内的文件全部写到 files 变量api.extendPackage
: 更改 package.json 内容api.postProcessFiles
: 添加 postProcess 钩子,在所有的 fileMiddleware 执行完再执行。在这个钩子可以获取所有 fileMiddleware 都执行完后的 file 变量。api.afterInvoke
: 添加 afterInvoke 钩子函数,在文件写入硬盘后调用api.afterAnyInvoke
:添加api.afterAnyInvoke
钩子函数,在文件写入硬盘后调用
小技巧
通过源码可知,我们可以直接更改 files 变量在增/减/修改
文件:
- 删除文件
delete api.generator.files.xxx
- 删除
dependencies
:delete this.pkg.dependencies.xxx
Hooks
查看 Generator.initPlugins
会发现,hooks 会被反复执行。
- 所有插件的 generator 调用前会执行一次,收集 hooks 内的
afterAnyHooks
(忽略 afterInvokeCbs) - 当前插件的 generator 调用后会执行一次,收集 hooks 内的
afterInvokeCbs
(忽略 afterAnyHooks) - 注意:generator 中调用
api.afterAnyInvoke
afterInvokeCbs
和afterAnyInvokeCbs
都会在文件写入硬盘后执行,afterInvokeCbs
会在afterAnyInvokeCbs
前执行
/**
* https://github1s.com/vuejs/vue-cli/blob/HEAD/packages/@vue/cli/lib/Generator.js#L150
* Generator.initPlugins
*/
async initPlugins () {
const { rootOptions, invoking } = this
const pluginIds = this.plugins.map(p => p.id)
// avoid modifying the passed afterInvokes, because we want to ignore them from other plugins
const passedAfterInvokeCbs = this.afterInvokeCbs
this.afterInvokeCbs = []
// apply hooks from all plugins to collect 'afterAnyHooks'
for (const plugin of this.allPlugins) {
const { id, apply } = plugin
const api = new GeneratorAPI(id, this, {}, rootOptions)
if (apply.hooks) {
await apply.hooks(api, {}, rootOptions, pluginIds)
}
}
// We are doing save/load to make the hook order deterministic
// save "any" hooks
const afterAnyInvokeCbsFromPlugins = this.afterAnyInvokeCbs
// reset hooks
this.afterInvokeCbs = passedAfterInvokeCbs
this.afterAnyInvokeCbs = []
this.postProcessFilesCbs = []
// 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 `allPlugins` loop above
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
// restore "any" hooks
this.afterAnyInvokeCbs = afterAnyInvokeCbsFromPlugins
}
// https://github1s.com/vuejs/vue-cli/blob/HEAD/packages/@vue/cli/lib/Creator.js#L223-L230
class Creator extends EventEmitter {
async create (cliOptions = {}, preset = null) {
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
for (const cb of afterInvokeCbs) {
await cb()
}
for (const cb of afterAnyInvokeCbs) {
await cb()
}
}
}
GeneratorAPI 使用实例
// 自定义插件的 generator.js 或者 generator/index.js
module.exports = (api, options) => {
api.render('./template')
api.extendPackage({
dependencies: {
'vue-router-layout': '^0.1.2'
}
})
// afterInvoke 钩子,这个钩子将在文件被写入硬盘之后被调用
api.afterInvoke(() => {})
}
// 自定义插件的 generator.js 或者 generator/index.js
module.exports.hooks = (api) => {
// hooks 函数会被反复执行
api.afterAnyInvoke(() => {
// 文件操作
})
// afterInvoke 钩子,这个钩子将在文件被写入硬盘之后被调用
api.afterInvoke(() => {
})
}
总结
本文从 Vue Create 命令入手,着重分析了 Vue CLI Generator 部分的的总体流程。总结下来,Generator 源码部分有下面几点可以借鉴:
- 用 files 变量保存文件信息,即减少文件的 IO 操作,也能方便统一管理文件结构和内容。
fileMiddlewares
插件和Hooks
钩子的设计,可以管理文件操作的顺序和方便的在各个时机添加新的处理逻辑。GeneratorAPI
将对外 API 独立成一个文件,功能组织更加清晰。
TLDR
vue create 用到的插件
执行 vue create 时,Vue CLI 首先会读取预设, 默认预设 信息如下所示:
exports.defaultPreset = {
useConfigFiles: false,
cssPreprocessor: undefined,
plugins: {
'@vue/cli-plugin-babel': {},
'@vue/cli-plugin-eslint': {
config: 'base',
lintOn: ['save']
}
}
}
可以看到,预设一般会包含一个或多个插件。在 creator 的源码中不难发现, @vue/cli-service 插件每次都会被加载。
// clone before mutating
preset = cloneDeep(preset)
// inject core service
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
结合 默认预设 和 creator 的源码 分析,默认情况下 vue create 会用到如下插件(Vue CLI 会确保 @vue/cli-service 插件第一个加载执行):