前言
上文简略介绍了 vue - V 的执行过程,本文在此基础上,持续剖析 vue create 的执行过程,理解 vue create <app-name> 经验了哪些过程?
注释
文件门路:/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 相干代码
...
.action((name, cmd) => {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.'))
}
// --git makes commander to default git to true
if (process.argv.includes('-g') || process.argv.includes('--git')) {options.forceGit = true}
require('../lib/create')(name, options)
})
commander
解析到 create <app-name>
命令,会主动执行 action
办法中的回调函数。首先是调用 cleanArgs
办法,它的作用是将传递的 Command 对象 cnd
,提取理论的选项到一个新对象中作为 options,次要是为了可读性,5.0 版本曾经将这个办法删除。间接传递 Command 对象作为options
变量,理解即可。而后就是校验命令输出的非 options 参数是否超过 1 个,若是则揭示用户,将应用第一个非 option 参数作为项目名称。其次是判断是否有 -g
选项并从新赋值 forceGit
防止歧义。最重要就是执行 /lib/create
函数。
create.js
文件门路:/packages/@vue/cli/lib/create.js
module.exports = (...args) => {return create(...args).catch(err => {stopSpinner(false) // do not persist
error(err)
if (!process.env.VUE_CLI_TEST) {process.exit(1)
}
})
}
执行文件定义 create
async 函数, 其中...args
残余参数能够作为参数传递的小技巧。如果抛出异样则执行 catch 函数。
create
async function create (projectName, options) {
// 是否有 --proxy,若有则定义 process.env.HTTP_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 || '.')
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)
}
// 判断生成指标文件夹是否存在且命令行没有 --merge 选项
if (fs.existsSync(targetDir) && !options.merge) {
// 有 --force 选项,间接移除已存在的指标文件夹
if (options.force) {await fs.remove(targetDir)
} else {await clearConsole()
if (inCurrent) {
// 1. 当前目录创立我的项目,询问是否持续,否则退出,是则持续
const {ok} = await inquirer.prompt([
{
name: 'ok',
type: 'confirm',
message: `Generate project in current directory?`
}
])
if (!ok) {return}
} else {
// 1. 非当前目录创立我的项目,给出 3 个抉择:Overwrite、Merge、Cancel
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}
]
}
])
// cancel 间接退出
if (!action) {return} else if (action === 'overwrite') {
// overwrite 删除原文件夹,抉择 merge 就是继续执行上面代码
console.log(`\nRemoving ${chalk.cyan(targetDir)}...`)
await fs.remove(targetDir)
}
}
}
}
const creator = new Creator(name, targetDir, getPromptModules())
await creator.create(options)
}
在当前目录创立我的项目, 会执行 vue create .
, 所以通过projectName === '.'
是否为当前目录。validateProjectName
函数则是校验项目名称是否合乎 npm 包名规范,如不合乎则正告并退出。接着就是校验要生成指标文件夹是否已存在,若已存在,则抉择是否继续执行(merge),或者间接退出执行,以及删除已存在文件夹继续执行生成我的项目。最终就是生成 Creator
实例 creator
,通过creator
的create
办法来创立新的我的项目。
Creator
文件门路:/packages/@vue/cli/lib/Creator.js
将创立我的项目的相干逻辑封装到了 Creator
类中。首先从构造函数开始
class Creator extends EventEmitter {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)
// 获取 cli/lib/promptModules 下所有函数,并传入 promptAPI 作为参数
promptModules.forEach(m => m(promptAPI))
}
}
实例化 Creator
类会接管 3 个参数,别离是 项目名称 name
, 生成指标文件夹门路 targetDir
, 以及promptModules
。这里传入的promptModules
是由 getPromptModules
函数生成。它会返回函数数组,别离动静援用了 cli/lib/promptModules
各个文件暴露出的函数,每个函数都会接管 PromptModuleAPI
实例做为 cli
参数。这里不过多介绍 PromptModuleAPI 类,你能够简略了解为对 creator 实例的二次封装。通过 PromptModuleAPI 实例来实现对 命令行交互提醒 相干逻辑的操作。
文件门路:/packages/@vue/cli/lib/utils/createTools.js
exports.getPromptModules = () => {
return [
'vueVersion',
'babel',
'typescript',
'pwa',
'router',
'vuex',
'cssPreprocessors',
'linter',
'unit',
'e2e'
].map(file => require(`../promptModules/${file}`))
}
其中 presetPrompt
与featurePrompt
由 Creator 类的 resolveIntroPrompts
办法生成。
resolveIntroPrompts () {
// 获取 preset 对象
const presets = this.getPresets()
// preset 对象转换成 array
const presetChoices = Object.entries(presets).map(([name, preset]) => {
let displayName = name
if (name === 'default') {displayName = 'Default'} else if (name === '__default_vue_3__') {displayName = 'Default (Vue 3 Preview)'
}
return {name: `${displayName} (${formatFeatures(preset)})`,
value: name
}
})
// 生成 presetPrompt
const presetPrompt = {
name: 'preset',
type: 'list',
message: `Please pick a preset:`,
choices: [
...presetChoices,
{
name: 'Manually select features',
value: '__manual__'
}
]
}
// 生成 featurePrompt
const featurePrompt = {
name: 'features',
when: isManualMode,
type: 'checkbox',
message: 'Check the features needed for your project:',
choices: [],
pageSize: 10
}
return {
presetPrompt,
featurePrompt
}
}
在 resolveIntroPrompts
函数中会首先执行 getPresets
办法来获取 preset。从这里开始就用到 option.js
相干的操作 loadOptions
。在 vue create
过程中保留的 preset 会被放在你的 home 目录下的一个配置文件中 (~/.vuerc
)。所以resolveIntroPrompts
函数会读取 .vuerc
的 preset。而后将默认内置的 defaults.presets
合并成残缺的 preset
列表对象。
exports.defaults = {
lastChecked: undefined,
latestVersion: undefined,
packageManager: undefined,
useTaobaoRegistry: undefined,
presets: {'default': Object.assign({ vueVersion: '2'}, exports.defaultPreset),
'__default_vue_3__': Object.assign({vueVersion: '3'}, exports.defaultPreset)
}
}
getPresets () {const savedOptions = loadOptions()
return Object.assign({}, savedOptions.presets, defaults.presets)
}
文件门路:/packages/@vue/cli/lib/options.js
let cachedOptions
exports.loadOptions = () => {
// 存在则返回缓存的 Options
if (cachedOptions) {return cachedOptions}
// rcPath 个别为用户目录下的.vuerc 文件的绝对路径
if (fs.existsSync(rcPath)) {
try {cachedOptions = JSON.parse(fs.readFileSync(rcPath, 'utf-8'))
} catch (e) {
error(
`Error loading saved preferences: ` +
`~/.vuerc may be corrupted or have syntax errors. ` +
`Please fix/delete it and re-run vue-cli in manual mode.\n` +
`(${e.message})`
)
exit(1)
}
validate(cachedOptions, schema, () => {
error(
`~/.vuerc may be outdated. ` +
`Please delete it and re-run vue-cli in manual mode.`
)
})
return cachedOptions
} else {return {}
}
}
- 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": {}}
}
outroPrompts
则是由 resolveOutroPrompts()
生成, 封装了命令行执行的结尾相干的 Prompt, 如是否拆分单个配置化文件,是否保留 preset 到 .vuerc
等。最初还会判断是否有设置默认包管理器,没有的话(换句话说,第一次执行vue create <app-name>
)则减少抉择默认包管理器的交互提醒。
resolveOutroPrompts () {
const outroPrompts = [
{
name: 'useConfigFiles',
when: isManualMode,
type: 'list',
message: 'Where do you prefer placing config for Babel, ESLint, etc.?',
choices: [
{
name: 'In dedicated config files',
value: 'files'
},
{
name: 'In package.json',
value: 'pkg'
}
]
},
{
name: 'save',
when: isManualMode,
type: 'confirm',
message: 'Save this as a preset for future projects?',
default: false
},
{
name: 'saveName',
when: answers => answers.save,
type: 'input',
message: 'Save preset as:'
}
]
// ask for packageManager once
const savedOptions = loadOptions()
if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {const packageManagerChoices = []
if (hasYarn()) {
packageManagerChoices.push({
name: 'Use Yarn',
value: 'yarn',
short: 'Yarn'
})
}
if (hasPnpm3OrLater()) {
packageManagerChoices.push({
name: 'Use PNPM',
value: 'pnpm',
short: 'PNPM'
})
}
packageManagerChoices.push({
name: 'Use NPM',
value: 'npm',
short: 'NPM'
})
outroPrompts.push({
name: 'packageManager',
type: 'list',
message: 'Pick the package manager to use when installing dependencies:',
choices: packageManagerChoices
})
}
return outroPrompts
}
create 办法
真正生成我的项目的逻辑则是调用了 creator
实例的 create
办法。由上文可知,cliOptions
是命令行的参数选项形成的对象,preset
不传默认是 null。所以会生成 preset
对象,代码中分为 4 种状况,这里剖析最初一种,通过 promptAndResolvePreset
办法生成 preset
对象。
async create (cliOptions = {}, preset = null) {
const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
const {run, name, context, afterInvokeCbs, afterAnyInvokeCbs} = this
if (!preset) {if (cliOptions.preset) {
// vue create foo --preset bar
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else if (cliOptions.default) {
// vue create foo --default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) {// 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 {preset = await this.promptAndResolvePreset()
}
}
// clone before mutating
preset = cloneDeep(preset)
// inject core service
preset.plugins['@vue/cli-service'] = Object.assign({projectName: name}, preset)
if (cliOptions.bare) {preset.plugins['@vue/cli-service'].bare = true
}
// legacy support for router
if (preset.router) {preset.plugins['@vue/cli-plugin-router'] = {}
if (preset.routerHistoryMode) {preset.plugins['@vue/cli-plugin-router'].historyMode = true
}
}
// Introducing this hack because typescript plugin must be invoked after router.
// Currently we rely on the `plugins` object enumeration order,
// which depends on the order of the field initialization.
// FIXME: Remove this ugly hack after the plugin ordering API settled down
if (preset.plugins['@vue/cli-plugin-router'] && preset.plugins['@vue/cli-plugin-typescript']) {const tmp = preset.plugins['@vue/cli-plugin-typescript']
delete preset.plugins['@vue/cli-plugin-typescript']
preset.plugins['@vue/cli-plugin-typescript'] = tmp
}
// legacy support for vuex
if (preset.vuex) {preset.plugins['@vue/cli-plugin-vuex'] = {}}
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
await clearConsole()
const pm = new PackageManager({context, forcePackageManager: packageManager})
log(`✨ Creating project in ${chalk.yellow(context)}.`)
this.emit('creation', { event: 'creating'})
// get latest CLI plugin version
const {latestMinor} = await getVersions()
// generate package.json with plugin dependencies
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {},
...resolvePkg(context)
}
const deps = Object.keys(preset.plugins)
deps.forEach(dep => {if (preset.plugins[dep]._isPreset) {return}
let {version} = preset.plugins[dep]
if (!version) {if (isOfficialPlugin(dep) || dep === '@vue/cli-service' || dep === '@vue/babel-preset-env') {version = isTestOrDebug ? `file:${path.resolve(__dirname, '../../../', dep)}` : `~${latestMinor}`
} else {version = 'latest'}
}
pkg.devDependencies[dep] = version
})
// write package.json
await writeFileTree(context, {'package.json': JSON.stringify(pkg, null, 2)
})
// generate a .npmrc file for pnpm, to persist the `shamefully-flatten` flag
if (packageManager === 'pnpm') {const pnpmConfig = hasPnpmVersionOrLater('4.0.0')
? 'shamefully-hoist=true\n'
: 'shamefully-flatten=true\n'
await writeFileTree(context, {'.npmrc': pnpmConfig})
}
if (packageManager === 'yarn' && semver.satisfies(process.version, '8.x')) {
// Vue CLI 4.x should support Node 8.x,
// but some dependenices already bumped `engines` field to Node 10
// and Yarn treats `engines` field too strictly
await writeFileTree(context, {'.yarnrc': '# Hotfix for Node 8.x\n--install.ignore-engines true\n'})
}
// intilaize git repository before installing deps
// so that vue-cli-service can setup git hooks.
const shouldInitGit = this.shouldInitGit(cliOptions)
if (shouldInitGit) {log(`🗃 Initializing git repository...`)
this.emit('creation', { event: 'git-init'})
await run('git init')
}
// install plugins
log(`⚙\u{fe0f} Installing CLI plugins. This might take a while...`)
log()
this.emit('creation', { event: 'plugins-install'})
if (isTestOrDebug && !process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {
// in development, avoid installation process
await require('./util/setupDevProject')(context)
} else {await pm.install()
}
// run generator
log(`🚀 Invoking generators...`)
this.emit('creation', { event: 'invoking-generators'})
const plugins = await this.resolvePlugins(preset.plugins, pkg)
const generator = new Generator(context, {
pkg,
plugins,
afterInvokeCbs,
afterAnyInvokeCbs
})
await generator.generate({extractConfigFiles: preset.useConfigFiles})
// install additional deps (injected by generators)
log(`📦 Installing additional dependencies...`)
this.emit('creation', { event: 'deps-install'})
log()
if (!isTestOrDebug || process.env.VUE_CLI_TEST_DO_INSTALL_PLUGIN) {await pm.install()
}
// run complete cbs if any (injected by generators)
log(`⚓ Running completion hooks...`)
this.emit('creation', { event: 'completion-hooks'})
for (const cb of afterInvokeCbs) {await cb()
}
for (const cb of afterAnyInvokeCbs) {await cb()
}
if (!generator.files['README.md']) {
// generate README.md
log()
log('📄 Generating README.md...')
await writeFileTree(context, {'README.md': generateReadme(generator.pkg, packageManager)
})
}
// commit initial state
let gitCommitFailed = false
if (shouldInitGit) {await run('git add -A')
if (isTestOrDebug) {await run('git', ['config', 'user.name', 'test'])
await run('git', ['config', 'user.email', 'test@test.com'])
await run('git', ['config', 'commit.gpgSign', 'false'])
}
const msg = typeof cliOptions.git === 'string' ? cliOptions.git : 'init'
try {await run('git', ['commit', '-m', msg, '--no-verify'])
} catch (e) {gitCommitFailed = true}
}
// log instructions
log()
log(`🎉 Successfully created project ${chalk.yellow(name)}.`)
if (!cliOptions.skipGetStarted) {
log(
`👉 Get started with the following commands:\n\n` +
(this.context === process.cwd() ? `` : chalk.cyan(` ${chalk.gray('$')} cd ${name}\n`)) +
chalk.cyan(` ${chalk.gray('$')} ${packageManager === 'yarn' ? 'yarn serve' : packageManager === 'pnpm' ? 'pnpm run serve' : 'npm run serve'}`)
)
}
log()
this.emit('creation', { event: 'done'})
if (gitCommitFailed) {
warn(
`Skipped git commit due to missing username and email in git config, or failed to sign commit.\n` +
`You will need to perform the initial commit yourself.\n`
)
}
generator.printExitLogs()}
- promptAndResolvePreset 办法
实质上就是应用 inquirer
库来执行 creator
实例中所有相干的 Prompt
, 通过用户抉择手动模式或预设模式来生成相应的preset
对象。
async promptAndResolvePreset (answers = null) {
// prompt
if (!answers) {await clearConsole(true)
// 外围代码
answers = await inquirer.prompt(this.resolveFinalPrompts())
}
debug('vue-cli:answers')(answers)
if (answers.packageManager) {
saveOptions({packageManager: answers.packageManager})
}
let preset
// 内置保留 preset
if (answers.preset && answers.preset !== '__manual__') {preset = await this.resolvePreset(answers.preset)
} else {
// 手动抉择我的项目各个个性
// manual
preset = {
useConfigFiles: answers.useConfigFiles === 'files',
plugins: {}}
answers.features = answers.features || []
// run cb registered by prompt modules to finalize the preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset))
}
// validate
validatePreset(preset)
// save preset
if (answers.save && answers.saveName && savePreset(answers.saveName, preset)) {log()
log(`🎉 Preset ${chalk.yellow(answers.saveName)} saved in ${chalk.yellow(rcPath)}`)
}
debug('vue-cli:preset')(preset)
return preset
}
其中 resolveFinalPrompts
函数就是获取所有相干的 Prompts。
resolveFinalPrompts () {
// patch generator-injected prompts to only show in manual mode
// 手动模式下才执行 injectedPrompts 数组提醒
this.injectedPrompts.forEach(prompt => {const originalWhen = prompt.when || (() => true)
prompt.when = answers => {return isManualMode(answers) && originalWhen(answers)
}
})
const prompts = [
this.presetPrompt,
this.featurePrompt,
...this.injectedPrompts,
...this.outroPrompts
]
debug('vue-cli:prompts')(prompts)
return prompts
}