上一篇文章(Vue-cli(3.0 + ) 源码分析(一)_夜跑者的博客-CSDN博客)分析了vue create my-project 命令执行过程中创建preset,往preset注入@vue/cli-service 插件,写入package.json,初始化git,npm install 依赖等过程。这篇文章继续往下分析核心部分:generate过程。
在分析generate过程之前,我们看看不做generate创建的工程项目是什么样的:
只是安装了cli-plugin-babel、cli-plugin-eslint、cli-service,package.json 只有几个字段。我们再看看实际创建一个可用的项目工程package.json 长什么样:
差距有点大,接下来看看generate过程是怎么做到的。
代码非常简单,处理一下preset中的插件,创建一个Generator实例,然后执行实例的generate方法,我们详细看下整个过程:
1. 处理preset中的插件
// { id: options } => [{ id, apply, options }]
async resolvePlugins (rawPlugins, pkg) {
// ensure cli-service is invoked first
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
const plugins = []
for (const id of Object.keys(rawPlugins)) {
//加载插件中的generator.js文件或/generator/index.js文件
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
let options = rawPlugins[id] || {}
if (options.prompts) {
//如果此插件中设置了prompts为true,则加载prompts对象。弹出交互对话框,让用户选择。
//例如vue create -p xxx test-project 命令中创建一个基于preset的工程项目
let pluginPrompts = loadModule(`${id}/prompts`, this.context)
if (pluginPrompts) {
const prompt = inquirer.createPromptModule()
if (typeof pluginPrompts === 'function') {
pluginPrompts = pluginPrompts(pkg, prompt)
}
if (typeof pluginPrompts.getPrompts === 'function') {
pluginPrompts = pluginPrompts.getPrompts(pkg, prompt)
}
log()
log(`${chalk.cyan(options._isPreset ? `Preset options:` : id)}`)
options = await prompt(pluginPrompts)
}
}
plugins.push({ id, apply, options })
}
return plugins
}
代码也比较简单,在主要位置增加了注释。主要是加载了插件的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) //对插件进行排序,See leetcode 210
this.originalPkg = pkg
this.pkg = Object.assign({}, pkg)
this.pm = new PackageManager({ context })
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 = [] //GeneratorAPI实例中的render方法会把插件中的模板文件回调函数推入到此数组
this.postProcessFilesCbs = []
// exit messages
this.exitLogs = []
// load all the other plugins
this.allPlugins = this.resolveAllPlugins() //把@vue/cli-service给过滤掉了
const cliService = plugins.find(p => p.id === '@vue/cli-service')
const rootOptions = cliService
? cliService.options
: inferRootOptions(pkg)
console.log('liubbc 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()
// 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)
}
generate方法非常短,先看一下initPlugins方法,initPlugins 主要有两个for循环:
// 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)
}
}
// 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)
}
}
第一个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 template files into the virtual files tree object.
*
* @param {string | object | FileMiddleware} source -
* Can be one of:
* - relative path to a directory;
* - Object hash of { sourceTemplate: targetFile } mappings;
* - a custom file middleware function.
* @param {object} [additionalData] - additional data available to templates.
* @param {object} [ejsOptions] - options for ejs.
*/
render (source, additionalData = {}, ejsOptions = {}) {
const baseDir = extractCallDir()
if (isString(source)) {
source = path.resolve(baseDir, source)
if(this.id.includes('cli-service')){
console.log('liubbc cli-service render source: ', source)
}
this._injectFileMiddleware(async (files) => {
const data = this._resolveData(additionalData)
const globby = require('globby')
const _files = await globby(['**/*'], { cwd: source, dot: true })
if(this.id.includes('cli-service')){
console.log('liubbc cli-service render files: ', _files)
}
for (const rawPath of _files) {
if(this.id.includes('cli-service')){
console.log('liubbc cli-service render rawPath: ', rawPath)
}
const targetPath = rawPath.split('/').map(filename => {
// dotfiles are ignored when published to npm, therefore in templates
// we need to use underscore instead (e.g. "_gitignore")
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)
if(this.id.includes('cli-service')){
console.log('liubbc cli-service render targetPath: ', targetPath, '; sourcePath: ', sourcePath)
}
const content = renderFile(sourcePath, data, ejsOptions)
// only set file if it's not all whitespace, or is a Buffer (binary files)
if (Buffer.isBuffer(content) || /[^\s]/.test(content)) {
files[targetPath] = content
}
}
if(this.id.includes('cli-service')){
console.log('liubbc cli-service render files: ', files)
}
})
_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 选项。
// handle imports and root option injections
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 里面的属性做个排序,把生成的虚拟文件数写到磁盘中
// 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)