无论是vue还是react项目,官方都提供了一个脚手架用来创建项目.但是我想要一个可以创建任何类型的项目.官方脚手架创建的项目启动页面都是显示一个图标加一些链接,这个模版我也想自定义.
但又对脚手架的实现知之甚少,在网上也没找到具体一些的vue-cli源码解析,所以只能自己努努力先从 vue-cli create 命令看起
主要使用到的第三方库
commander inquirer execa ejs
内置模块path fs
- vue-cli是通过
lerna
管理多包项目.在packages文件夹下cli
包,找到其下的package.json中bin
指向的文件(bin/vue.js),create 命令在此注册.
js
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-m, --packageManager <command>', 'Use specified npm client when installing
//...
.option('-f, --force', 'Overwrite target directory if it exists')
.action((name, options) => {
//...
require('../lib/create')(name, options) //进入这个文件
})
这里会判断文件名是否合法,不合法退出并弹出提示.接着检查工作目录下是否已经存在相同名字的文件,如果有弹出提示
在这里选择
overwrite
或在create命令后加了 -f
,会直接删除之前的文件.
之后利用 creator
类创建一个对象,并调用其 create
方法.先看 getPromptModules
返回了什么.
js
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
这里不就是自定义模板时选择的 插件
吗
js
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
拿其中的路由举例.他会导出一个函数,在函数内部他会调用传入参数的方法.
- injectFeature 注入功能,让我们记住选了这个插件
- injectPrompt 选择完路由之后,还要选择模式,需要这个函数,所以上面有些文件没有这个函数
- onPromptComplete 选择完之后的回调

现在可以看creator的构造函数了.
- presetPrompt 选择预设的交互,选项包括用户自己的预设(预设信息存在一个名为
.vuerc
的文件),脚手架内置的两个预设和自定义选项 - featurePrompt 选择插件的交互(现在里面的插件列表还是空的)
- outroPrompts 三个交互,这是最后询问我们 Where do you prefer placing config...,是否将其设为预设,如果保存还要输入名字
js
constructor (name, context, promptModules) {
super()
this.name = name //项目名
this.context = process.env.VUE_CLI_CONTEXT = context //路径
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
this.presetPrompt = presetPrompt
this.featurePrompt = featurePrompt
this.outroPrompts = this.resolveOutroPrompts()
this.injectedPrompts = []
this.promptCompleteCbs = []
this.afterInvokeCbs = []
this.afterAnyInvokeCbs = []
this.run = this.run.bind(this)
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
}
看上面最后两行,这里又new了一个 PromptModuleAPI 对象,它里面的函数跟promptModules中调用的函数相同.
循环之后
featurePrompt 里的插件列表就有了
injectedPrompts 就是选择完插件还需要选择配置的交互也收集起来了
promptCompleteCbs 表示插件相关的交互完成之后的回调也收集起来.

紧接着执行creator对象的 create 函数,再执行其中的 promptAndResolvePreset
在这个函数里利用 inquirer
库来做交互. resolveFinalPrompts 函数会返回一个数组,包含创建项目的一系列交互.例如选择预设,如果是自定义下个问题就是选择插件.
js
answers = await inquirer.prompt(this.resolveFinalPrompts())
选择完这些就可以进入下一步,下一步当然解析用户的选项,先声明一个变量 preset
js
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
执行之前的回调数组,会根据用户的选项给 preset 添加信息,也就是.
js
preset : {
useConfigFiles: true,
plugins: {
"@vue/cli-plugin-babel": {
},
"@vue/cli-plugin-router": {
historyMode: false,
},
"@vue/cli-plugin-vuex": {
},
"@vue/cli-plugin-eslint": {
config: "base",
lintOn: [
"save",
],
},
},
vueVersion: "3",
}
//最后还要加上下面这个插件
// inject core service
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
接着又会new一个PackageManager对象,这个暂时不看
js
const pm = new PackageManager({ context, forcePackageManager: packageManager })
现在需要创建package.josn文件了,里面的内容 pkg 初始化是
js
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context) //如果已经存在package.json,也就是之前选择merge的情况下,会合并之前的package.json和现在用户选择的插件等信息.
}
现在根据之前的preset变量可以给pkg添加依赖,添加之后的结果是
js
{
name: "projectname",
version: "0.1.0",
private: true,
devDependencies: {
"@vue/cli-plugin-babel": "~5.0.0",
"@vue/cli-plugin-router": "~5.0.0",
"@vue/cli-plugin-vuex": "~5.0.0",
"@vue/cli-plugin-eslint": "~5.0.0",
"@vue/cli-service": "~5.0.0",
},
}
根据pkg可以生成我们的package.json文件,这里是使用 writeFileTree 函数,其中使用node的fs模块就可以生成文件了.
js
// write package.json
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
现在需要下载依赖,调用上面的PackageManager对象 pm 的install进行依赖下载 其实也就是利用第三方库execa开启子进程执行命令npm install,这里vue默认是pnpm.
依赖下载完毕之后,需要生成项目目录文件.进入下一个最为核心的环节 Generator
js
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
await generator.generate({
extractConfigFiles: preset.useConfigFiles
})
第一步:resolvePlugins
,接收两个参数,一个是选择的预设插件数组,第二个就是上面生成package.josn的数据了.这个函数的作用是把插件的数据格式对象变成一个数组, { id: options } => [{ id, apply, options }]
,多了一个apply,他是从什么地方来呢?
js
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
apply就是拿到下载的依赖node_modules/@vue下模块导出的方法,这个以后要用先跟对应的插件存在一起.

第二步new一个Generator类,构造函数里传入了 pkg,plugins 等.执行generator上的generate方法.在这个函数里执行initPlugins初始化插件信息.
js
initPlugins(){
//...
// 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)
//...
}
}
这里又new了一个GeneratorAPI
对象,将他作为参数传递给之前保存的 apply 方法
拿其中的cli-service举个例子,执行apply函数其中有调用api,也就是
GeneratorAPI
的实例中的render方法,再执行其中的 _injectFileMiddleware
注册middleware.
再执行extendPackage,他的作用是扩展package.json中相应的配置,执行完毕之后退出插件初始化完毕.
接着 resolveFiles 也就是循环执行middleware会根据插件名称找到node_modules对应插件下的template内容保存下来
writeFileTree 会渲染新的package.json和模版文件,最后再次下载依赖.
现在就可以知道模版不是从git仓库拿到的,而是在下载的插件提供的,并且还会拓展package.json的内容
最后的最后会生成readme文件,初始化git,打印log.
总结
首先判断项目名是否合法,是否已经存在同名文件 --> 在.vuerc文件获取用户预设和脚手架自带两个预设以及自定义预设供用户选择 --> 这里选择自定义 --> 出现选择插件的交互,选择之后还有与插件配置相关的交互,全部选择完成之后收集到用户数据answer --> 拿到answer生成package.json --> 利用execa库执行下载依赖的命令 --> 根据选择的插件名找到下载的node_modules中对应的插件,执行其下的index.js文件会添加package.josn中的相关内容还会保存其中的template文件 --> 生成新的package.son文件和模版文件 --> 再次下载依赖 --> 最后会生成readme文件,初始化git,打印log.