Vue-cli 的 create 指令是如何创建项目文件的

Vue-cli 是创建 Vue 项目的一个好方法,之前只是使用,没有关注过内部结构是咋样的。最近在做一个组内项目的 cli 工具,参考了一下 Vue-cli 的实现方法。

Vue-cli 项目结构

Vue-cli,是一个多 package 项目,使用 lerna 进行管理。package 内的 @vue 文件夹下有很多包:

vue-cli-create-01.png

其中与创建项目有关的包,我们主要需要关注 cli 和 cli-service。

@vue/cli

@vue/cli 包,就是我们平时执行的 bin 文件,入口是 @vue/cli/bin/vue.js。该文件会先检查用户当前使用的 node.js 版本是否符合要求。

之后使用 commander.js来提供命令行指令接口。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
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('-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')
.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 会帮助收集选项参数等内容,之后调用了

1
require('../lib/create')(name, options)

可以看到最终项目创建逻辑是在 ../lib/create 中执行的。

../lib/create

create.js 文件主要做了以下几件事:

  1. 检查项目初始化名称是否合法。
  2. 检查项目初始化目录是否存在,若存在提示用户选择覆盖、合并、取消进行操作。
  3. 初始化 Creator 类,并执行实例的 .create 方法。

Creator 类

我们直接来看一下 Creator 类的 create 方法,该方法主要做了以下事情:

  1. 初始化 preset

    preset 用于指定初始化项目的具体配置,例如是否使用 babel、typescript 等。preset 可以通过执行命令行工具时指定,未指定时会在命令行中提供选项让使用者选择。

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    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()
    }
    }

    之后会在 preset 中添加 @vue/cli-service 插件,该插件用于创建初始化项目的基础内容。

    1
    2
    3
    4
    5
    6
    // inject core service
    preset.plugins['@vue/cli-service'] = Object.assign({
    projectName: name
    }, preset, {
    bare: cliOptions.bare
    })
  2. 根据命令行选项,初始化git。

  3. 安装所有插件。

    preset 初始化之后,就会生成 package.json 文件,所有插件作为依赖放在 package.json 中,使用 npm 或 yarn 来进行安装。

  4. 初始化 Generator 类,并执行实例的 generate 方法。

    Generator 类用于提供插件的加载机制。加载所有插件的内容,放入初始化的项目中。我们之后会说。

  5. 插件内容加载完毕后,会生成新的 package.json 依赖,此时需要再次进行安装。

  6. 生成 README.md 文件。

  7. 进行第一次 git 提交。

  8. 命令行中打印初始化完成提示信息。

Generator 类

Generator 类是 Vue-cli 提供的与插件之间交流机制,编写插件时可利用这个机制,进行文件、package.json 依赖的修改,具体可见官方文档:https://cli.vuejs.org/dev-guide/plugin-dev.html#generator

它是怎么工作的呢?Generator 类初始化时,会给每个插件初始化一个 GeneratorAPI 类,作为插件与 cli 之间的接口。

1
2
3
4
5
// apply generators from plugins
plugins.forEach(({ id, apply, options }) => {
const api = new GeneratorAPI(id, this, options, rootOptions)
apply(api, options, rootOptions, invoking)
})

之后在 .generate 方法调用时,调用了自身的 .resolveFiles 方法,来获取所有的文件内容。

1
2
// wait for file resolve
await this.resolveFiles()

文件内容获取后,进行 package.json 的写入,以及模板文件的写入:

1
2
3
this.files['package.json'] = JSON.stringify(this.pkg, null, 2) + '\n'
// write/update file tree to disk
await writeFileTree(this.context, this.files, initialFiles)