获取预设选项

在开始分析之前简单描述下什么是 vue-cli-preset一个 Vue CLI preset 是一个包含创建新项目所需预定义选项和插件的 JSON 对象,让用户无需在命令提示中选择它们:

{
  "useConfigFiles": true,
  "router": true,
  "vuex": true,
  "cssPreprocessor": "sass",
  "plugins": {
    "@vue/cli-plugin-babel": {},
    "@vue/cli-plugin-eslint": {
      "config": "airbnb",
      "lintOn": ["save", "commit"]
    }
  }
}

更多关于 preset 可以前往 vue-cli 官网 插件和 Preset#

在基础验证完成以后会创建一个 Creator 实例:

const creator = new Creator(name, targetDir, getPromptModules())

getPromptModules

在分析 Creator 之前先看下 getPromptModules() 获取到的是什么。getPromptModules() 获取了 babel,typescript,pwa,router,vuex, cssPreprocessors,linter,unit,e2e 的 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'] = {}
    }
  })
}

cli.injectFeature

cli.injectFeature 是注入 featurePrompt,即初始化项目时选择 babel,typescript,pwa 等等,如下图:

cli.injectPrompt

cli.injectPrompt 是根据选择的 featurePrompt 然后注入对应的 prompt,当选择了 unit,接下来会有以下的 prompt,选择 Mocha + Chai 还是 Jest

cli.onPromptComplete

cli.onPromptComplete 就是一个回调,会根据选择来添加对应的插件, 当选择了 mocha ,那么就会添加 @vue/cli-plugin-unit-mocha 插件。

new Creator()

搞清楚了 getPromptModules 之后,下面开始看一下初始化 Creator 实例发生了什么,直接看代码:

constructor (name, context, promptModules) {
    super()
    this.name = name
    this.context = process.env.VUE_CLI_CONTEXT = context
    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts() // 获取了 presetPrompt list,在初始化项目的时候提供选择
    this.presetPrompt = presetPrompt // presetPrompt list
    this.featurePrompt = featurePrompt // babal, pwa, e2e etc.
    this.outroPrompts = this.resolveOutroPrompts() //  存放项目配置的文件(package.json || congfig.js) 以及是否将 presetPrompts 存放起来
    this.injectedPrompts = [] // 对应 feature 的 Prompts
    this.promptCompleteCbs = [] // injectedPrompts 的回调
    this.createCompleteCbs = []

    this.run = this.run.bind(this)

    const promptAPI = new PromptModuleAPI(this)

    /**
     * 1. 将 babel, e2e, pwa 等 push 到 featurePrompt.choices 中,在选择项目需要配置哪些时显示出来 (checkbox);
     * 2. 将 babel, e2e, pwa 等 push 到 injectedPrompts 中,当设置了 feature 会对应通过 Prompts 来进一步选择哪种模式,比如当选择了 E2E Testing ,然后会再次让你
     *    选择哪种 E2E Testing,即, Cypress (Chrome only) ||  Nightwatch (Selenium-based);
     * 3. 将每中 feature 的 onPromptComplete push 到 promptCompleteCbs,在后面会根据选择的配置来安装对应的 plugin。
     */
    promptModules.forEach(m => m(promptAPI))
  }

这段代码主要看下 PromptModuleAPI,源码如下:

module.exports = class PromptModuleAPI {
  constructor (creator) {
    this.creator = creator
  }

  injectFeature (feature) {
    this.creator.featurePrompt.choices.push(feature)
  }

  injectPrompt (prompt) {
    this.creator.injectedPrompts.push(prompt)
  }

  injectOptionForPrompt (name, option) {
    this.creator.injectedPrompts.find(f => {
      return f.name === name
    }).choices.push(option)
  }

  onPromptComplete (cb) {
    this.creator.promptCompleteCbs.push(cb)
  }
}

PromptModuleAPI 实例会调用它的实例方法,然后将 injectFeatureinjectPromptinjectOptionForPromptonPromptComplete保存到 Creator实例对应的变量中。

最后遍历 getPromptModules 获取的 promptModules,传入实例 promptAPI,初始化 Creator 实例中 featurePrompt, injectedPrompts, promptCompleteCbs 变量。

getPreset

在创建一个 Creator 实例后,然后调用了 create 方法

await creator.create(options)

create 开始是获取 preset ,源码如下:

const isTestOrDebug = process.env.VUE_CLI_TEST || process.env.VUE_CLI_DEBUG
console.log('before creating......')
// name: demo
// context: targetDir
const { run, name, context, createCompleteCbs } = 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) { // 使用内联的 JSON 字符串预设选项
    // 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 {
    // eg: vue create demo
    preset = await this.promptAndResolvePreset()
  }
}

// clone before mutating
preset = cloneDeep(preset)
// inject core service
preset.plugins['@vue/cli-service'] = Object.assign({ // 注入核心 @vue/cli-service
  projectName: name
}, preset, {
  bare: cliOptions.bare
})

先判断 vue create 命令是否带有 -p 选项,如果有的话会调用 resolvePreset 去解析 preset。resolvePreset 函数会先获取 ~/.vuerc 中保存的 preset, 然后进行遍历,如果里面包含了 -p 中的 <presetName>,则返回~/.vuerc 中的 preset。如果没有则判断是否是采用内联的 JSON 字符串预设选项,如果是就会解析 .json 文件,并返回 preset,还有一种情况就是从远程获取 preset(利用 download-git-repo 下载远程的 preset.json)并返回。

上面的情况是当 vue create 命令带有 -p 选项的时候才会执行,如果没有就会调用 promptAndResolvePreset 函数利用 inquirer.prompt 以命令后交互的形式来获取 preset,下面看下 promptAndResolvePreset 函数的源码:

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
  if (answers.preset && answers.preset !== '__manual__') { // 如果是选择使用本地保存的 preset (~/.vuerc)
    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)
  }
  
  debug('vue-cli:preset')(preset)
  return preset
}

在调用 inquirer.prompt 之前利用 this.resolveFinalPrompts() 获取了最后的 prompts,到这里有些同学可能就有点晕了,到底有多少个 prompt,别急,下面将 简单介绍下,查看 this.resolveFinalPrompts() 源码:

resolveFinalPrompts () {
  // patch generator-injected prompts to only show in manual mode
  // 将所有的 Prompt 合并,包含 preset,feature,injected,outro,只有当选择了手动模式的时候才会显示 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
}

比较容易的就可以看出作用就是将 presetPrompt, featurePrompt, injectedPrompts, outroPrompts 合并成一个数组进行返回,这几个 Prompt 的含义如下:

  • presetPrompt: 预设选项 prompt,当上次以 Manually 模式进行了预设选项,并且保存到了 ~/.vuerc 中,那么在初始化项目时就会列出已经保存的 preset,并提供选择。
  • featurePrompt:项目的一些 feature,就是选择 babel,typescript,pwa,router,vuex,cssPreprocessors,linter,unit,e2e。
  • injectedPrompts:当选择了 feature 后,就会为对应的 feature 注入 prompts,比如你选择了 unit,那么就会让你选择模式: Mocha + Chai 还是 Jest
  • outroPrompts: 其他的 prompt,包含:
    • 将 Babel, PostCSS, ESLint 等等的配置文件存放在 package.json 中还是存放在 config 文件中;
    • 是否需要将这次设置的 preset 保存到本地,如果需要则会进一步让你输入名称进行保存;
    • 安装依赖是选择 npm 还是 yarn。

inquirer.prompt 执行完成后会返回 answers,如果选择了本地保存的 preset 或者 default,则调用 resolvePreset 进行解析 preset,否则遍历 promptCompleteCbs 执行 injectFeature 和 injectPrompt 的回调,将对应的插件赋值到 options.plugins 中,以 unit 为例:

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'] = {}
  }
})

如果 feature 选择了 unit,并且 unit 模式选择的是 Mocha + Chai,则添加 @vue/cli-plugin-unit-mocha 插件,如果选择的是 Jest 则添加 @vue/cli-plugin-unit-jest 插件。

在获取到 preset 之后,还会向 preset 的插件里面注入核心插件 @vue/cli-service, 它是调用 vue-cli-service <command> [...args] 时创建的类。 负责管理内部的 webpack 配置、暴露服务和构建项目的命令等。

到这里获取预设选项(preset)大致分析完了,在下节将会分析依赖的安装。