CLI 2.x生成的项目有build和config目录与webpack打包相关, 如下
// 图1 2
旧版本下, 使用CLI 创建项目之后, 相关配置就本地化了, 未来如果想要升级webpack, 必须自己来升级
Vue CLI 3将webpack相关的打包配置内置在一个npm包@vue/cli-service中, 由官方维护, 更加省事. 提供了修改webpack配置的接口, 可以是在vue.config.js中配置, 或者在插件中调用.
webpack-chain 提供链式调用API来生成/修改webpack配置
一个运行时依赖 (@vue/cli-service),该依赖: 可升级; 基于 webpack 构建,并带有合理的默认配置; 可以通过项目内的配置文件进行配置; 可以通过插件进行扩展。
// 图3
旧版本下, 如果想要增加新功能, 比如组内使用easy-tool同步多语言, 我们需要自己在项目中增加easy-tool配置文件等操作
脚手架肯定是有在项目中生成指定文件的能力, Vue CLI 3有插件系统, 并给插件提供了接口, 可以在项目中去生成指定文件, 给项目增加easy-tool配置文件这些可以自动化操作的事情可以在插件中完成, 组内成员可以给项目安装上插件, 执行相关操作去调用插件.
Vue CLI 3 插件 这里看
当然, 不止这些
相关工具包
commander node.js 命令行接口的完整解决方案
inquirer interactive command line, 命令行交互相关
webpack-chain 提供链式调用API来生成/修改webpack配置
fs-extra 系统fs模块的扩展,提供了更多便利的 API,并继承了fs模块的 API
execa Process execution for humans, improves child_process
ejs 模板引擎, 类似的有Jade / Pug, mustache
deepmerge, 合并两个对象的可枚举属性
semver (Semantic Versioning) 用于npm版本号的语义化操作
dotenv loads environment variables from a .env file into process.env
recast 将代码转换成ast, 修改ast等
vue create
@vue/cli包提供vue [options] 命令 可以通过 vue create 快速创建一个新项目 preset.json: 包含预设配置数据的主要文件
// 本地preset
vue create -p ./preset.json dolphin_template
// 加载远程preset.json
要生成一个项目, 需要按照这样的流程. 首先官方给我们一个最初的模板(由官方插件提供), 然后, 我们写一些vue-cli插件, 在官方模板的基础上, 缝缝补补, 最后得到符合我们要求的初始项目.
preset中可以通过plugins指定插件, 从而在官方默认模版的基础上, 去新加/覆盖一些文件, 修改配置文件(vue.config.js, babel.config.js等), 在package.json中注入script命令等.
##preset.json
{
"plugins": {
"@vue/cli-plugin-babel": {
"version": "^3.5.1"
},
"@vue/cli-plugin-eslint": {
"version": "^3.5.1",
"config": "standard",
"lintOn": [
"save"
]
},
"vue-cli-plugin-dolphin-base": {
"version": "2.6.5",
"prompts": true
},
"vue-cli-plugin-dolphin-theme": {
"version": "2.6.5"
},
"vue-cli-plugin-changelog": {
"version": "2.6.5"
},
"vue-cli-plugin-easytool": {
"version": "2.6.5"
},
"vue-cli-plugin-easymock": {
"version": "2.6.5",
"prompts": true
},
"vue-cli-plugin-lego": {
"version": "2.6.5"
}
}
}
不使用任何外部插件
如果我们在preset.json中没有添加任何插件, 利用vue create 生成的项目是什么样的? 没有主动添加任何插件所生成的项目是最简单的, 对于理解vue create很有帮助
图四
上面命令行中的流程, 可以用下面的图来概括, 项目目录和package.json的变化也在图中, 可以放大看
图图图图
命令行工具的执行代码在哪
vue create的相关代码在@vue/cli包中,
图
@vue/cli/bin/vue.js 配合package.json的bin字段使用
https://docs.npmjs.com/files/package.json#bin
##Execute vue create
@vue/cli/bin/vue.js
const program = require('commander')
// ...
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('-c, --clone', 'Use git clone when fetching remote preset')
.action((name, cmd) => {
const options = cleanArgs(cmd)
// ...一些边界条件处理
require('../lib/create')(name, options)
})
//...
program.parse(process.argv)
使用commander来进行命令行参数处理, 能够
获取用户输入的相关配置参数options
获取所要创建的项目名name, 假设为dolphin_template
在这之后, 开始创建一个项目的正式流程:
1. 校验, 获取输出目录
validateProjectName, 比如用户可能用vue-cli创建通用库放npm上, 库名称是需要出现在url中的, 避免使用non-url-safe characters, 从而规避一些风险 目标目录dolphin_tempalte 是否存在? 根据实际情况, 提示overwrite/merge/cancel (inquirer.js) cli通过commander收集到的信息包括: 参数配置信息, 项目名称, 以及targetDir @vue/cli/lib/create.js
const cwd = options.cwd || process.cwd()
const inCurrent = projectName === '.'
const name = inCurrent ? path.relative('../', cwd) : projectName // 项目目录名称
const targetDir = path.resolve(cwd, projectName || '.')
targetDir是个很重要的信息, 后续的依赖安装, 生成模板, 获取插件目录, 依赖查找等, 都需要基于这个上下文.
2.获取preset信息
这里只讨论与vue create -p相关的逻辑, 由于指定了想要安装的插件, 所以一些prompt相关的逻辑可以忽略. 解析preset/inline preset, 确定所要加载的插件, 如果没有使用preset, 则通过询问prompt来确定. @vue/cli/lib/Creator.js
// 使用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()
}
3. 确定使用的包管理器
优先级: 命令行参数packageManager > .vuerc中packageManager > yarn > pnpm > npm 在 vue create 过程中保存的预设配置会被放在你的 home 目录下的一个配置文件中 (~/.vuerc)。你可以通过直接编辑这个文件来调整、添加、删除保存好的配置。 cli.vuejs.org/zh/guide/pl…
const packageManager = (
cliOptions.packageManager ||
loadOptions().packageManager ||
(hasYarn() ? 'yarn' : null) ||
(hasPnpm3OrLater() ? 'pnpm' : 'npm')
)
const pm = new PackageManager({ context, forcePackageManager: packageManager }
4. 注入内部插件
通过preset获取到用户要加载的插件列表, 并加入内部插件@vue/cli-service
// 加入内部插件, 设置其options(projectName, preset中的属性)
preset.plugins['@vue/cli-service'] = Object.assign({
projectName: name
}, preset)
5.确定插件version, 生成package.json
对于非官方插件, 优先使用preset中定义version, 否则使用latest 对于官方插件, 优先使用preset中定义的version, 否则使用最新的minor版本, 若@vue/cli monorepo的版本为3.2.4, 则使用^3.2.0. 因为可以认为monorepo维护的minor版本是统一的, 各个子repo path版本有差异
const pkg = {
name,
version: '0.1.0',
private: true,
devDependencies: {}
}
//...
const deps = Object.keys(preset.plugins)
pkg.devDependencies[dep] = (
preset.plugins[dep].version || ((/^@vue/.test(dep)) ? `^${latestMinor}` : `latest`)
)
在项目名称dolphin_template文件夹, 下生成package.json
await writeFileTree(context, {
'package.json': JSON.stringify(pkg, null, 2)
})
async function writeFileTree (dir, files, previousFiles) {
// ...
Object.keys(files).forEach((name) => {
const filePath = path.join(dir, name)
fs.ensureDirSync(path.dirname(filePath)) // 不存在则创建项目文件夹
fs.writeFileSync(filePath, files[name])
})
}
6. git init
如果环境中有git, 默认会执行git init
7. Install dep
子进程执行yarn/ npm intall等安装package.json中的依赖(包括 @vue/cli-service, preset中配置的plugins)
log(`⚙ Installing CLI plugins. This might take a while...`)
execa('npm', ['install']) // 简化版
8. resolve plugins && invoke plugins
后续需要利用插件生成相关模板, preset.plugins是保存了插件信息的对象, 其中有在preset.json中定义的plugins, 也包括所注入的内部插件@vue/cli-service, 需要保证@vue/cli-service生成模板的逻辑最先执行. vue-cli利用Object.keys()来实现对象key的排序. 对象形式的plugins
{
"@vue/cli-service": {},
// "vue-cli-plugin-dolphin-base": {
// "version": "2.6.5",
// "prompts": true
// },
//...
}
规范定义了Object.keys()输出顺序
var foo = {a: 1, b:2, c: 3}
var bar = {}
bar.c = foo.c
delete foo.c
Object.keys(foo).forEach(key => {
bar[key] = foo[key]
})
// Object.keys(foo) // ['a', b']
// Object.keys(bar) // ['c', 'a', 'b']
对象foo, 对于非integer like的字符串(a,b,c), 按照属性创建的顺序排.
Firstly, integer-like keys in ascending order
Secondly, normal keys in insertion order
Then, Symbols in insertion order
Lastly, if mixed, order: interger-like, normal keys, Symbols
https://tc39.es/ecma262/#sec-ordinaryownpropertykeys
https://2ality.com/2015/10/property-traversal-order-es6.html
https://zhuanlan.zhihu.com/p/40601459
https://juejin.cn/post/6844903796062191624
@vue/cli/lib/Creator.js
async resolvePlugins (rawPlugins) {
// ensure cli-service is invoked first
rawPlugins = sortObject(rawPlugins, ['@vue/cli-service'], true)
const plugins = []
for (const id of Object.keys(rawPlugins)) {
const apply = loadModule(`${id}/generator`, this.context) || (() => {})
let options = rawPlugins[id] || {}
if (options.prompts) {
// ...
}
plugins.push({ id, apply, options })
}
return plugins
}
对象形式的plugins排序后, 生成对应数组
{
"@vue/cli-service": {
//...
},
// "vue-cli-plugin-dolphin-base": {
// "version": "2.6.5",
// "prompts": true
// },
//...
}
已排序, 数组形式的plugins
[
{
id: '@vue/cli-service'
apply: require(require.resolve('@vue/cli-service/generator', context)),
options: {
//...
}
}
]
插件提供模板, 按顺序调用vue-cli-*插件的模板生成逻辑.
// apply generators from plugins
for (const plugin of this.plugins) {
const { id, apply, options } = plugin
const api = new GeneratorAPI(id, this, options, rootOptions)
await apply(api, options, rootOptions, invoking)
// ...
}
各个插件都可能提供模板, 遍历记录下来, 使用一个对象记录要生成文件的路径和内容信息, 存在覆盖的情况
官网一个插件generator例子
一个典型的 CLI 插件的目录结构看起来是这样的:
├── README.md
├── generator.js # generator (可选)
├── prompts.js # prompt 文件 (可选)
├── index.js # service 插件
└── package.json