跳到主要内容Vue CLI 3.0+ 源码分析:Generate 过程详解 | 极客日志JavaScriptNode.js大前端
Vue CLI 3.0+ 源码分析:Generate 过程详解
Vue CLI 3.0+ 的 generate 流程主要包括插件解析、Generator 实例化、模板渲染及文件写入。通过 resolvePlugins 加载插件配置,利用 GeneratorAPI 扩展配置和渲染 EJS 模板,最终生成虚拟文件树并持久化到磁盘,完成项目脚手架的构建。
上篇文章分析了 vue create 命令的 preset 创建、依赖安装等初始化过程。本文继续深入核心部分:generate 流程。
在分析 generate 过程之前,我们看看不做 generate 创建的工程项目是什么样的:

只是安装了 cli-plugin-babel、cli-plugin-eslint、cli-service,package.json 只有几个字段。我们再看看实际创建一个可用的项目工程 package.json 长什么样:

差距有点大,接下来看看 generate 过程是怎么做到的。

代码非常简单,处理一下 preset 中的插件,创建一个 Generator 实例,然后执行实例的 generate 方法,我们详细看下整个过程:
1. 处理 preset 中的插件
async resolvePlugins(rawPlugins, pkg) {
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
const plugins = []
for (const id of Object.keys(rawPlugins)) {
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
let options = rawPlugins[id] || {}
(options.) {
pluginPrompts = (, .)
(pluginPrompts) {
prompt = inquirer.()
( pluginPrompts === ) {
pluginPrompts = (pkg, prompt)
}
( pluginPrompts. === ) {
pluginPrompts = pluginPrompts.(pkg, prompt)
}
()
()
options = (pluginPrompts)
}
}
plugins.({ id, apply, options })
}
plugins
}
相关免费在线工具
- Keycode 信息
查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online
- Escape 与 Native 编解码
JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online
- JavaScript / HTML 格式化
使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online
- JavaScript 压缩与混淆
Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online
- Base64 字符串编码/解码
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
- Base64 文件转换器
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
if
prompts
let
loadModule
`${id}/prompts`
this
context
if
const
createPromptModule
if
typeof
'function'
pluginPrompts
if
typeof
getPrompts
'function'
getPrompts
log
log
`${chalk.cyan(options._isPreset ? 'Preset options:' : id)}`
await
prompt
push
return
代码也比较简单,在主要位置增加了注释。主要是加载了插件的 generator.js(generator/index.js) 中 export 出来的方法赋值给 apply,如果插件带有 prompts,则要处理 prompts 把结果赋值给 options。最后把插件的三个元素{id, apply, options}组成 object 推入数组。从这里也能推断出之后会遍历这些插件并且执行插件的 apply 方法。
2. 创建 Generator 实例
module.exports = class Generator {
constructor(
context,
{
pkg = {},
plugins = [],
afterInvokeCbs = [],
afterAnyInvokeCbs = [],
files = {},
invoking = false
} = {}
) {
this.context = context
this.plugins = sortPlugins(plugins)
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
this.files = Object.keys(files).length
if (files) {
watchFiles(files, (this.filesModifyRecord = new Set()))
}
else {
this.files = files
}
this.fileMiddlewares = []
this.postProcessFilesCbs = []
this.exitLogs = []
this.allPlugins = this.resolveAllPlugins()
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService ? cliService.options : inferRootOptions(pkg)
console.log('rootOptions: ', rootOptions)
this.rootOptions = rootOptions
}
}
new 一个 Generator 实例,在构造函数中,把之前处理后的 plugins 赋值给 Generator 实例的 plugins 属性。如果插件中有@vue/cli-service 则 rootOptions 采用@vue/cli-service 的 options。
3. 执行 Generator 实例的 generate 方法
async generate({ extractConfigFiles = false, checkExisting = false } = {}) {
await this.initPlugins()
const initialFiles = Object.assign({}, this.files)
this.extractConfigFiles(extractConfigFiles, checkExisting)
await this.resolveFiles()
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)
}
generate 方法非常短,先看一下 initPlugins 方法,initPlugins 主要有两个 for 循环:
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)
}
}
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) {
await apply.hooks(api, options, rootOptions, pluginIds)
}
}
第一个 for 循环主要处理 allPlugins 插件,如果插件存在 hooks 则执行 hooks,不详细阐述了。第二个 for 循环为每个插件创建了一个 GeneratorAPI 实例,并执行插件的 apply 方法,之前分析过 apply 方法就是插件的 generator.js(generator/index.js)export 出来的方法,需要注意的是 apply 方法的第一个参数是 GeneratorAPI 实例。我们自己想实现一个插件,在插件中就可以调用 GeneratorAPI 实例提供的方法。GeneratorAPI 实例提供了哪些方法呢?
- hasPlugin:判断项目中是否存在此插件
- extendPackage:扩展 package.json 配置
- render:利用 ejs 渲染模板文件
- genJSConfig:将 json 文件生成为 js 配置文件
- injectImports:向文件当中注入 import 语法的方法
- injectRootOptions:向 Vue 根实例中添加选项
我们看一下@vue/cli-service 中的 generator.js:
调用 GeneratorAPI 实例的 extendPackage 方法往 package.json 里面扩展一些选项;重点看一下调用的 render 方法。
render(source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
if (isString(source)) {
source = path.resolve(baseDir, source)
this._injectFileMiddleware(async (files) => {
const data = this._resolveData(additionalData)
const globby = require('globby')
const _files = await globby(['**/*'], { cwd: source, dot: true })
for (const rawPath of _files) {
const targetPath = rawPath.split('/').map(filename => {
if (filename.charAt(0) === '_' && filename.charAt(1) !== '_') {
return `.${filename.slice(1)}`
}
if (filename.charAt(0) === '_' && filename.charAt(1) === '_') {
return `${filename.slice(1)}`
}
return filename
}).join('/')
const sourcePath = path.resolve(source, rawPath)
const content = renderFile(sourcePath, data, ejsOptions)
if (Buffer.isBuffer(content) || /[\s]/.test(content)) {
files[targetPath] = content
}
}
})
}
_injectFileMiddleware(middleware) {
this.generator.fileMiddlewares.push(middleware)
}
}
注释 Render template files into the virtual files tree object. 说的比较明白,具体看下怎么做的。
@vue/cli-service 的调用 render 方法传入的第一个参数是字符串'./template',是个模板相对路径,把它转换为绝对路径。用 globby 提取出该路径下所有的文件:
接下来通过 ejs.render 把文件中的内容处理一下并提取出来生成虚拟文件树对象:
接下来就是将 generator 注入的 import 和 rootOption 解析到对应的文件中,比如选择了 vuex,会在 src/main.js 中添加 import store from './store',以及在 vue 根实例中添加 router 选项。
Object.keys(files).forEach(file => {
let imports = this.imports[file]
imports = imports instanceof Set ? Array.from(imports) : imports
if (imports && imports.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectImports'),
{ imports }
)
}
let injections = this.rootOptions[file]
injections = injections instanceof Set ? Array.from(injections) : injections
if (injections && injections.length > 0) {
files[file] = runTransformation(
{ path: file, source: files[file] },
require('./util/codemods/injectOptions'),
{ injections }
)
}
})
最后把 package.json 里面的属性做个排序,把生成的虚拟文件数写到磁盘中
this.sortPkg()
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
await writeFileTree(this.context, this.files, initialFiles, this.filesModifyRecord)