前言
上文简略介绍了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 cachedOptionsexports.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}