前言

上文简略介绍了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)    }  })}

执行文件定义createasync函数,其中...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,通过creatorcreate办法来创立新的我的项目。

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}`))}

其中presetPromptfeaturePrompt由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}