前言
由于平时工作比较忙,基本上都是利用课余时间写文章的,vue-cli 源码分析这个系列比较长,估计要耗费比较长的时间才能写完,但为了大家能提前看到,所以文章会一直持续更新。前言、开始等部分目前还没有,就直接从代码分析开始,后续文章写完之后,会补上,请大家见谅。如果大家喜欢的话,请给我一个赞或者评论下,谢谢。
开始
vue create
create 入口
vue create 命令的入口在 packages/@vue/cli/bin/vue.js 中。
program
.command('create <app-name>')
.description('create a new project powered by vue-cli-service')
.option('-p, --preset <presetName>', 'Skip prompts and use saved or remote preset')
.option('-d, --default', 'Skip prompts and use default preset')
.option('-i, --inlinePreset <json>', 'Skip prompts and use inline JSON string as preset')
.option('-m, --packageManager <command>', 'Use specified npm client when installing dependencies')
.option('-r, --registry <url>', 'Use specified npm registry when installing dependencies (only for npm)')
.option('-g, --git [message]', 'Force git initialization with initial commit message')
.option('-n, --no-git', 'Skip git initialization')
.option('-f, --force', 'Overwrite target directory if it exists')
.option('--merge', 'Merge target directory if it exists')
.option('-c, --clone', 'Use git clone when fetching remote preset')
.option('-x, --proxy', 'Use specified proxy when creating project')
.option('-b, --bare', 'Scaffold project without beginner instructions')
.option('--skipGetStarted', 'Skip displaying "Get started" instructions')
.action((name, cmd) => {
// 返回 options 对象
const options = cleanArgs(cmd)
if (minimist(process.argv.slice(3))._.length > 1) {
console.log(chalk.yellow('\n Info: You provided more than one argument. The first one will be used as the app\'s name, the rest are ignored.'))
}
// -g,--git,则 options.forceGit 为 true
if (process.argv.includes('-g') || process.argv.includes('--git')) {
options.forceGit = true
}
require('../lib/create')(name, options)
})
部分参数的解释(来源于 vue-cli 官网)。
-p, --preset <presetName> 忽略提示符并使用已保存的或远程的预设选项
-d, --default 忽略提示符并使用默认预设选项
-i, --inlinePreset <json> 忽略提示符并使用内联的 JSON 字符串预设选项
-m, --packageManager <command> 在安装依赖时使用指定的 npm 客户端
-r, --registry <url> 在安装依赖时使用指定的 npm registry
-g, --git [message] 强制 / 跳过 git 初始化,并可选的指定初始化提交信息
-n, --no-git 跳过 git 初始化
-f, --force 覆写目标目录可能存在的配置
-c, --clone 使用 git clone 获取远程预设选项
-x, --proxy 使用指定的代理创建项目
-b, --bare 创建项目时省略默认组件中的新手指导信息
-h, --help 输出使用帮助信息
当我们从命令行参数中获取到了 options 对象,然后就去加载 packages/@vue/cli/lib/create.js 并执行。
下一节我们将整体介绍 vue create 命令由哪几部分构成。
整体分析
先通过一张图来整体感受下 vue create 的整体过程:
可以看到整个过程还是比较复杂的,为了便于讲解,我把整个过程划分为五个部分:
- 基础验证
- 获取预设选项
- 依赖安装
- Generator
- 结尾分析
基础验证
当我们加载了 create.js 之后,会预先定义几个变量:
// -x, --proxy
if (options.proxy) {
process.env.HTTP_PROXY = options.proxy
}
// 当前工作目录
const cwd = options.cwd || process.cwd()
// 项目名称是否为 .
const inCurrent = projectName === '.'
// 当前项目名称
const name = inCurrent ? path.relative('../', cwd) : projectName
// 当前项目的绝对路径
const targetDir = path.resolve(cwd, projectName || '.')
其中 cwd 表示当前的工作目录,inCurrent 表示当前的项目名称是否为 . ,name 表示当前的项目名称,targetDir 表示当前项目的绝对路径。
声明完变量之后,紧接着就判断 name 是否符合 npm 包名规范,如果不符合的话,就会打印相应的 error 信息,程序并终止。
// 验证项目名称是否符合 npm 包名规范
const result = validateProjectName(name)
if (!result.validForNewPackages) {
console.error(chalk.red(`Invalid project name: "${name}"`))
result.errors && result.errors.forEach(err => {
console.error(chalk.red.dim('Error: ' + err))
})
result.warnings && result.warnings.forEach(warn => {
console.error(chalk.red.dim('Warning: ' + warn))
})
exit(1)
}
validate-npm-package-name 是判断项目名称是否符合 npm 包名规范的库,由 npm 团队负责维护。
如果包名符合规范,就走下面的逻辑。
// targetDir 已经存在且无 --merge
if (fs.existsSync(targetDir) && !options.merge) {
// -f, --force
if (options.force) {
// 删除 targetDir
await fs.remove(targetDir)
} else {
await clearConsole()
if (inCurrent) {
// 当前项目名称为 .
const { ok } = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {
return
}
} else {
// 当前项目名称不为 .
const { action } = await inquirer.prompt([
{
name: 'action',
type: 'list',
message: `Target directory ${chalk.cyan(targetDir)} already exists. Pick an action:`,
choices: [
{ name: 'Overwrite', value: 'overwrite' },
{ name: 'Merge', value: 'merge' },
{ name: 'Cancel', value: false }
]
}
])
if (!action) {
// 选择 Cancel,退出
return
} else if (action === 'overwrite') {
// 选择 Overwrite,则删除 targetDir
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
为了比较直观的看懂这段代码,我特意画了一个流程图方便大家理解。
获取预设选项
先来了解下什么是 Preset。
Preset
一个 Vue CLI Preset 是一个包含创建新项目所需预定义选项和插件的 JSON 对象,让用户无需在命令提示中选择它们。
在 vue create 过程中保存的 preset 会被放在你的 home 目录下的一个配置文件中(~/.vuerc)。你可以通过直接编辑这个文件夹来调整、添加、删除保存好的 preset。
这里有一个 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 可以前往 vue-cli 官网 插件和 Preset。
基础验证完成之后会创建一个 Creator 实例。
// 创建 Creator 实例
const creator = new Creator(name, targetDir, getPromptModules())
getPromptModules
getPromptModules 的代码在 packages/@vue/cli/lib/util/createTool.js 中。
exports.getPromptModules = () => {
return [
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
getPromptModules() 获取到的是 babel、cssPreprocessor、e2e、linter、pwa、router、typescript、unit、vuex 的 Prompt 的配置信息。以 unit 为例:
module.exports = cli => {
cli.injectFeature({
name: 'Unit Testing',
value: 'unit',
short: 'Unit',
description: 'Add a Unit Testing solution like Jest or Mocha',
link: 'https://cli.vuejs.org/config/#unit-testing',
plugins: ['unit-jest', 'unit-mocha']
})
cli.injectPrompt({
name: 'unit',
when: answers => answers.features.includes('unit'),
type: 'list',
message: 'Pick a unit testing solution:',
choices: [
{
name: 'Mocha + Chai',
value: 'mocha',
short: 'Mocha'
},
{
name: 'Jest',
value: 'jest',
short: 'Jest'
}
]
})
cli.onPromptComplete((answers, options) => {
if (answers.unit === 'mocha') {
options.plugins['@vue/cli-plugin-unit-mocha'] = {}
} else if (answers.unit === 'jest') {
options.plugins['@vue/cli-plugin-unit-jest'] = {}
}
})
}
injectFeature
cli.injectFeature 是注入 featurePrompt,即初始化项目时选择的 Babel、TypeScript、Router 等等,如下图:
injectPrompt
cli.injectPrompt 是根据选择的 featurePrompt,然后注入相应的 prompt。如当我们选择了 unit,接下来会让我们选择 Mocha + Chai 还是 Jest,如下图:
onPromptComplete
cli.onPromptComplete 是一个回调,根据用户的选择添加对应的 plugin。如当我们选择了 Mocha + Chai,就会添加 @vue/cli-plugin-unit-mocha,如当我们选择了 Jest,就会添加 @vue/cli-plugin-unit-jest。
new Creator()
接下来我们来看初始化 Creator 实例做的事情:
constructor (name, context, promptModules) {
super()
// 项目名称
this.name = name
// 项目的绝对路径
this.context = process.env.VUE_CLI_CONTEXT = context
const { presetPrompt, featurePrompt } = this.resolveIntroPrompts()
// preset list
this.presetPrompt = presetPrompt
// babel、typescript、pwa 等
this.featurePrompt = featurePrompt
// 额外的 prompt
// 存放项目配置文件、是否保存 preset、选择 package manager
this.outroPrompts = this.resolveOutroPrompts()
// feature 对应的 prompt
this.injectedPrompts = []
// injectedPrompts 的回调
this.promptCompleteCbs = []
this.afterInvokeCbs = []
this.afterAnyInvokeCbs = []
this.run = this.run.bind(this)
// featurePrompt 中的 choices 中添加数据
// injectedPrompts 中添加数据
// promptCompleteCbs 中添加数据
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
}
在 constructor 中,我们定义了一些实例属性(含义已注明),除了定义基本属性之外,最重要的是如下:
// featurePrompt 中的 choices 中添加数据
// injectedPrompts 中添加数据
// promptCompleteCbs 中添加数据
const promptAPI = new PromptModuleAPI(this)
promptModules.forEach(m => m(promptAPI))
PromptModuleAPI 的源码在 packages/@vue/cli/lib/PromptModuleAPI.js
module.exports = class PromptModuleAPI {
constructor (creator) {
// Creator 实例
this.creator = creator
}
// 往 creator.featurePrompt 中的 choices 添加 feature
injectFeature (feature) {
this.creator.featurePrompt.choices.push(feature)
}
// 往 creator.injectedPrompts 中添加 prompt
injectPrompt (prompt) {
this.creator.injectedPrompts.push(prompt)
}
// 往 creator.injectedPrompts 中的某个 prompt 的 choices 添加 option
injectOptionForPrompt (name, option) {
this.creator.injectedPrompts.find(f => {
return f.name === name
}).choices.push(option)
}
// 往 creator.promptCompleteCbs 中添加 cb
onPromptComplete (cb) {
this.creator.promptCompleteCbs.push(cb)
}
}
PromptModuleAPI 中定义的 creator 就是我们创建的 Creator 实例,同时还定义了几个方法 injectFeature、injectPrompt、injectOptionForPrompt、onPromptComplete 都是方便操作 creator 中的 featurePrompt 、injectedPrompts、promptCompleteCbs。
当我们创建完 PromptModuleAPI 实例后,紧接着我们遍历 getPromptModules 获取的 promptModules,传入 PromptModuleAPI 实例,初始化 Creator 实例中 featurePrompt、injectedPrompts、promptCompleteCbs。到此 Creator 构造函数执行完毕。
getPreset
当创建完一个 Creator 实例之后,紧接着就调用它的 create 方法。
// 执行 creator.create 函数
await creator.create(options)
create 函数代码比较多,我们分步来看,首先是获取 preset。
// test or debug
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
const { run, name, context, afterInvokeCbs, afterAnyInvokeCbs } = this
// preset 为 null
if (!preset) {
if (cliOptions.preset) {
// 提供preset,如:vue create foo --preset bar
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else if (cliOptions.default) {
// 默认preset,如:vue create foo --default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) {
// 内联preset,如:vue create foo --inlinePreset {...}
try {
preset = JSON.parse(cliOptions.inlinePreset)
} catch (e) {
error(`CLI inline preset is not valid JSON: ${cliOptions.inlinePreset}`)
exit(1)
}
} else {
// manual,如:vue create foo
preset = await this.promptAndResolvePreset()
}
}
// clone before mutating
preset = cloneDeep(preset)
// 注入 @vue/cli-service
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
// -b, --bare,无新手指引的脚手架项目
if (cliOptions.bare) {
preset.plugins['@vue/cli-service'].bare = true
}
// router 的旧版本支持
if (preset.router) {
preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {
preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// vuex 的 旧版本支持
if (preset.vuex) {
preset.plugins['@vue/cli-plugin-vuex'] = {}
}
今日到此为止,2020-08-13 23:53 深夜