3-3、Vue cli 详解

前端脚手架:快速创建项目的基础代码框架和配置的工具

Vue cli 目前处于维护状态

使用介绍:介绍 | Vue CLI

官方源码:https://github.com/vuejs/vue-cli/tree/dev/packages/%40vue

Vue3.0 推荐使用 create-vue 来创建项目

Vue cli 使用流程

创建项目 => 选择配置 => 安装依赖

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
// 创建项目
$ vue create yourProjectName

// 选择模板
Vue CLI v5.0.8
? Please pick a preset: (Use arrow keys)
Default ([Vue 3] babel, eslint)
Default ([Vue 2] babel, eslint)
Manually select features

// 手动配置
Vue CLI v5.0.8
? Please pick a preset: Manually select features
? Check the features needed for your project: (Press <space> to select, <a> to
toggle all, <i> to invert selection, and <enter> to proceed)
❯◉ Babel
TypeScript
Progressive Web App (PWA) Support
Router
Vuex
CSS Pre-processors
Linter / Formatter
Unit Testing
E2E Testing

// 拉取项目并安装依赖
Vue CLI v5.0.8
Creating project in /Users/hzq/code/mianshi/3-1 Vue 基础/v2-project.
🗃 Initializing git repository...
⚙️ Installing CLI plugins. This might take a while...

🚀 Invoking generators...
📦 Installing additional dependencies...

Running completion hooks...

📄 Generating README.md...

🎉 Successfully created project v2-project.
👉 Get started with the following commands:

$ cd v2-project
$ pnpm run serve

// 运行项目
$ pnpm run serve

Vue cli 基础原理

vue 指令是如何提供的?
如何实现与用户交互的?
如何生成代码文件的?

Vue cli 2.x 版本

项目目录:

首先vue命令是在哪里注入到全局的呢?

package.json代码:

1
2
3
4
5
6
7
8
{
// ...
"bin": {
"vue": "bin/vue", // 这里将 vue 注入到全局
"vue-init": "bin/vue-init",
"vue-list": "bin/vue-list"
},
}

所以我们才可以直接用vue命令,比如vue -h、vue -V

其次bin/vue里面定义了更多的子命令,代码如下:

1
2
3
4
5
6
7
8
9
#!/usr/bin/env node

require('commander')
.version(require('../package').version)
.usage('<command> [options]')
.command('init', 'generate a new project from a template')
.command('list', 'list available official templates')
.command('build', 'prototype a new project')
.parse(process.argv)

bin/vue代码解析:

1
2
3
4
5
6
7
8
9
10
11
12
#!/user/bin/env node // 特殊注释,告诉操作系统使用 node 来执行该文件
require('commander') // 引入 commander 库,用于解析命令行参数
.version(require('../package').version) // 设置版本
.usage('<command> [options]') // 定义默认信息,用户输入 -h 时显示这个信息
.commnad('init', 'generate a new project from a template') // 定义 init 子命令,后面的是它的描述
.command('list', 'list available official templates') // 定义 list 子命令,后面的是它的描述
.command('build', 'prototype a new project') // 定义 build 子命令,后面的是它的描述
.parse(process.argv) // 使用 parse 解析命令行参数,process.argv 是包含命令行参数的数组
// 最后 commander 会设置全局变量来反映这些参数

// 总的来说,这段代码定义了一个简单的命令行工具,它有三个子命令:init, list, 和 build,
// 并为每个子命令提供了简短的描述。用户可以通过这些子命令和相关的选项与工具进行交互。

主流程

主流程代码是在bin/vue-init里面

对应代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
#!/usr/bin/env node
// npm 包依赖项 -- start
// download: 下载 git 仓库代码工具,类似于 git clone xxx
const download = require("download-git-repo");
// program: 命令行工具: 定义命令和选项,并自动生成帮助和用法信息
const program = require("commander");
// fs: 文件系统(File System)内置模块,可操作本地文件的读取、写入、创建、删除、复制等
const exists = require("fs").existsSync;
// path: 内置模块,处理文件路径,可拼接、解析、转换和格式化文件路径
const path = require("path");
// ora: 在命令行界面(CLI)中显示加载动画的
const ora = require("ora");
// user-home: 用于获取用户根目录
const home = require("user-home");
// tildify: 用于将绝对路径转换为使用波浪符(~)表示的相对路径。
const tildify = require("tildify");
// chalk: 命令行高亮工具
const chalk = require("chalk");
// inquirer: 命令行交互式问答工具,可以用来向用户提问并获取输入。
const inquirer = require("inquirer");
// rm: 用于删除文件和目录
const rm = require("rimraf").sync;
// npm 包依赖项 -- end

// 本地依赖项 -- start
// logger: 打印工具
const logger = require("../lib/logger");
// generate: 代码生成工具: 基于模板生成本地代码/文件
const generate = require("../lib/generate");
// check-version: 检测版本工具: 1、检查 node 版本;2、提示更新 cli 版本(通过拉取npm包的版本对比得出)
const checkVersion = require("../lib/check-version");
// warnings: 警告提示工具
const warnings = require("../lib/warnings");
// local-path: 路径工具
const localPath = require("../lib/local-path");
// 是否为本地路径方法
const isLocalPath = localPath.isLocalPath;
// 获取本地模板路径方法
const getTemplatePath = localPath.getTemplatePath;
// 本地依赖项 -- end
/**
* Usage.
*/

// 定义该命令的使用格式,若用户只输入 vue init
// 则会提示:
// Usage: vue init <template-name> [project-name]
// Commands:
// -c, --clone use git clone
// --offline use cached template
program
.usage("<template-name> [project-name]")
.option("-c, --clone", "use git clone")
.option("--offline", "use cached template");

/**
* Help.
*/

program.on("--help", () => {
console.log(" Examples:");
console.log();
console.log(
chalk.gray(" # create a new project with an official template")
);
console.log(" $ vue init webpack my-project");
console.log();
console.log(
chalk.gray(" # create a new project straight from a github template")
);
console.log(" $ vue init username/repo my-project");
console.log();
});

/**
* Help.
*/

function help() {
program.parse(process.argv);
if (program.args.length < 1) return program.help();
}
help();

/**
* Settings.
*/
// 根据输入的命令行,进行模板名称、文件名称等边缘检测与处理
// program.args 返回命令行输入的参数数组
// 模板名称
let template = program.args[0];
// hasSlash: 是否包含斜杠 => 模板名称是否包含路径层级 => 本质是判断是否为 github 的第三方模板
// 因为 github 的第三方模板名称格式为:username/repo
const hasSlash = template.indexOf("/") > -1;
// 项目名称
const rawName = program.args[1];
// 输入空的项目名称 => 表明创建的文件需要平铺在当前文件夹下
const inPlace = !rawName || rawName === ".";
// 文件夹名称:如果 inPlace 为 true,则 ../ 回退一级,否则继续使用 rawName
const name = inPlace ? path.relative("../", process.cwd()) : rawName;
// 生成的文件夹路径
const to = path.resolve(rawName || ".");
const clone = program.clone || false;

// 拼接本地模板的存放路径,都放在本地根路径下的 .vue-templates 文件夹内
const tmp = path.join(home, ".vue-templates", template.replace(/\//g, "-"));
if (program.offline) {
console.log(`> Use cached template at ${chalk.yellow(tildify(tmp))}`);
template = tmp;
}

/**
* Padding.
*/

console.log();
process.on("exit", () => {
console.log();
});
if (exists(to)) {
// 要生成的项目名已存在时:
inquirer
.prompt([
{
type: "confirm",
message: inPlace
? "Generate project in current directory?"
: "Target directory exists. Continue?",
name: "ok",
},
])
.then((answers) => {
if (answers.ok) {
run();
}
})
.catch(logger.fatal);
} else {
run();
}

/**
* Check, download and generate the project.
*/
// 核心代码:处理使用本地还是远程模板
function run() {
// isLocalPath:通过输入的模板名称来判断是否为本地的模板
// 比如输入的完整命令为:vue init ../.local/code/mianshi/vue-template vue2Project,则就是用本地的模板
if (isLocalPath(template)) {
// 根据模板名称获取完整的模板地址
const templatePath = getTemplatePath(template);
if (exists(templatePath)) {
// 存在该地址,则使用它去生成代码文件
// name: 命令行输入的项目名称
// templatePath: 本地模板的地址
// to: 项目生成的地址
generate(name, templatePath, to, (err) => {
if (err) logger.fatal(err);
console.log();
logger.success('Generated "%s".', name);
});
} else {
// 本地模板没找到
logger.fatal('Local template "%s" not found.', template);
}
} else {
// 非本地模板时
// 比如输入的完整命令为:vue init vue-template vue2Project,则就是用远程的模板
checkVersion(() => {
if (!hasSlash) {
// 模板名不带/时,则代表使用的是官方模板
// 使用官方模板名称:vuejs-templates 开头
const officialTemplate = "vuejs-templates/" + template;
if (template.indexOf("#") !== -1) {
// 模板名带#时,表明模板可用
downloadAndGenerate(officialTemplate);
} else {
// 模板名不带#时,可能是老版本、不可用、废弃等
if (template.indexOf("-2.0") !== -1) {
// 模板名带-2.0时,会显示警告,该版本已被废弃等....
warnings.v2SuffixTemplatesDeprecated(template, inPlace ? "" : name);
return;
}

// warnings.v2BranchIsNowDefault(template, inPlace ? '' : name)
// 表明不是废弃版本,即可能是老版本,就继续下载&生成
downloadAndGenerate(officialTemplate);
}
} else {
// 模板名带/时,则代表使用的是 github 的第三方模板
downloadAndGenerate(template);
}
});
}
}

/**
* Download a generate from a template repo.
*
* @param {String} template
*/
// 核心代码:使用远程模板生成代码文件
function downloadAndGenerate(template) {
// 命令行 loading
const spinner = ora("downloading template");
spinner.start();
// Remove if local template exists
if (exists(tmp)) rm(tmp);
// download():下载远程模板代码
// template:远程的模板名称
// tmp:官方模板本地存储位置
download(template, tmp, { clone }, (err) => {
spinner.stop();
if (err)
logger.fatal(
"Failed to download repo " + template + ": " + err.message.trim()
);
// generate():生成代码文件
// name: 命令行输入的项目名称
// tmp: 刚刚下载的官方模板的本地地址
// to: 项目生成的地址
generate(name, tmp, to, (err) => {
if (err) logger.fatal(err);
console.log();
logger.success('Generated "%s".', name);
});
});
}

流程图

关键点

脚手架的版本检测

vue cli 的版本检测代码如下:

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
27
28
29
30
31
32
33
34
35
36
37
38
39
const request = require('request')
const semver = require('semver') // 用于处理和解析语义化版本号
const chalk = require('chalk')
const packageConfig = require('../package.json')

module.exports = done => {
// --- 系统的 nodejs 版本与脚手架指定的版本比较 ---(标准版)
// semver.satisfies(): 是否满足
// process.version: 当前运行的 node 版本
// packageConfig.engines.node: cli 指定的 node 版本
if (!semver.satisfies(process.version, packageConfig.engines.node)) {
return console.log(chalk.red(
' You must upgrade node to >=' + packageConfig.engines.node + '.x to use vue-cli'
))
}

// --- 系统的脚手架版本与最新脚手架的版本比较 ---
// 请求远程 npmjs 上的 vue-cli 包的最新版本号
// vs
// 当前系统已安装的 vue-cli 包源代码的 package.json 的版本
request({
url: 'https://registry.npmjs.org/vue-cli',
timeout: 1000
}, (err, res, body) => {
if (!err && res.statusCode === 200) {
const latestVersion = JSON.parse(body)['dist-tags'].latest
const localVersion = packageConfig.version
// semver.lt(): 是否低于
if (semver.lt(localVersion, latestVersion)) {
console.log(chalk.yellow(' A newer version of vue-cli is available.'))
console.log()
console.log(' latest: ' + chalk.green(latestVersion))
console.log(' installed: ' + chalk.red(localVersion))
console.log()
}
}
done()
})
}

vue list 命令

vue-list.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
#!/usr/bin/env node

const logger = require("../lib/logger"); // 自定义打印方法
const request = require("request"); // HTTP 请求库
const chalk = require("chalk"); // 命令行高亮工具

/**
* Padding.
*/

console.log();
process.on("exit", () => {
console.log();
});

/**
* List repos.
*/

// 通过 request 库,去拉取 github 上面的官方模板数据,不会展示本地缓存的模板数据
request(
{
url: "https://api.github.com/users/vuejs-templates/repos",
headers: {
"User-Agent": "vue-cli",
},
},
(err, res, body) => {
if (err) logger.fatal(err); // 请求有错误时,打印展示错误
const requestBody = JSON.parse(body);
if (Array.isArray(requestBody)) {
console.log(" Available official templates:");
console.log();
requestBody.forEach((repo) => {
// 请求的模板数据循环,然后展示到命令行内
console.log(
" " +
chalk.yellow("★") +
" " +
chalk.blue(repo.name) +
" - " +
repo.description
);
});
} else {
console.error(requestBody.message);
}
}
);

Vue cli 进阶知识

代码生成逻辑

generate.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
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
// npm 包依赖项 -- start
// chalk: 命令行高亮工具
const chalk = require("chalk");
// metalsmith: 一个静态内容生成器,用于处理文件和目录。
const Metalsmith = require("metalsmith");
// handlebars: 基于 JavaScript 的模板引擎,用于生成动态 HTML 或其他格式的文本文件。
const Handlebars = require("handlebars");
// async: 一个提供了许多异步操作辅助函数的库,如 eachSeries、waterfall 等。
const async = require("async");
// consolidate: 是一个用于在 Express 中使用多种模板引擎的库。导入了 consolidate 库中的 handlebars 渲染函数。
const render = require("consolidate").handlebars.render;
// path: 内置模块,处理文件路径,可拼接、解析、转换和格式化文件路径
const path = require("path");
// multimatch: 多条件匹配工具
const multimatch = require("multimatch");
// npm 包依赖项 -- end

// 本地依赖项 -- start
// getOptions: 用于获取或解析命令行选项
const getOptions = require("./options");
// ask: 用于向用户提问并获取输入
const ask = require("./ask");
// filter: 过滤或处理文件列表
const filter = require("./filter");
// logger: 打印工具
const logger = require("./logger");
// 本地依赖项 -- end

// register handlebars helper
Handlebars.registerHelper("if_eq", function (a, b, opts) {
return a === b ? opts.fn(this) : opts.inverse(this);
});

Handlebars.registerHelper("unless_eq", function (a, b, opts) {
return a === b ? opts.inverse(this) : opts.fn(this);
});

/**
* Generate a template given a `src` and `dest`.
*
* @param {String} name 命令行输入的项目名称
* @param {String} src 下载的官方模板的本地地址(某个模板的线上地址: https://github.com/vuejs-templates/webpack)
* @param {String} dest 项目生成的地址
* @param {Function} done 完成的回调函数
*/
module.exports = function generate(name, src, dest, done) {
// 1. 读取配置项,读取模板内的 meta.json || meta.js
const opts = getOptions(name, src);
// 2. 使用 Metalsmith 初始化数据,拿到模板里面的 template 文件(里面的内容就是生成出来的代码文件)
const metalsmith = Metalsmith(path.join(src, "template"));
// 3. 配置项合并: metalsmith.metadata 的元数据 与 手动定义的数据 进行合并
const data = Object.assign(metalsmith.metadata(), {
destDirName: name,
inPlace: dest === process.cwd(),
noEscape: true,
});
// 4. 注册 handlebars 的 helper
opts.helpers &&
Object.keys(opts.helpers).map((key) => {
Handlebars.registerHelper(key, opts.helpers[key]);
});

const helpers = { chalk, logger };

// 5. 调用 before 钩子函数
if (opts.metalsmith && typeof opts.metalsmith.before === "function") {
opts.metalsmith.before(metalsmith, opts, helpers);
}

metalsmith
.use(askQuestions(opts.prompts)) // 问询主流程: name|description|author|router|lint ...
.use(filterFiles(opts.filters)) // 根据问询结果过滤掉不需要的文件
.use(renderTemplateFiles(opts.skipInterpolation)); // 最后生成模板文件

if (typeof opts.metalsmith === "function") {
// 执行
opts.metalsmith(metalsmith, opts, helpers);
} else if (opts.metalsmith && typeof opts.metalsmith.after === "function") {
// 调用 after 钩子函数
opts.metalsmith.after(metalsmith, opts, helpers);
}

// 结尾
metalsmith
.clean(false)
.source(".") // start from template root instead of `./src` which is Metalsmith's default for `source`
.destination(dest)
.build((err, files) => {
done(err);
// 调用 complete 钩子函数: 依赖排序、安装依赖、运行 lint、打印最终信息
if (typeof opts.complete === "function") {
const helpers = { chalk, logger, files };
opts.complete(data, helpers);
} else {
logMessage(opts.completeMessage, data);
}
});

return data;
};

/**
* Create a middleware for asking questions.
*
* @param {Object} prompts
* @return {Function}
*/

function askQuestions(prompts) {
return (files, metalsmith, done) => {
ask(prompts, metalsmith.metadata(), done);
};
}

/**
* Create a middleware for filtering files.
*
* @param {Object} filters
* @return {Function}
*/

function filterFiles(filters) {
return (files, metalsmith, done) => {
filter();
};
}

/**
* Template in place plugin.
*
* @param {Object} files
* @param {Metalsmith} metalsmith
* @param {Function} done
*/

function renderTemplateFiles(skipInterpolation) {
skipInterpolation =
typeof skipInterpolation === "string"
? [skipInterpolation]
: skipInterpolation;
return (files, metalsmith, done) => {
const keys = Object.keys(files);
const metalsmithMetadata = metalsmith.metadata();
// 异步处理[模板/template]下的每一个文件
async.each(
keys,
(file, next) => {
// skipping files with skipInterpolation option
if (
skipInterpolation &&
multimatch([file], skipInterpolation, { dot: true }).length // 多重匹配,满足 [file], skipInterpolation, { dot: true } 时
) {
return next();
}
// 获取文件内容字符串
const str = files[file].contents.toString();
// do not attempt to render files that do not have mustaches
if (!/{{([^{}]+)}}/g.test(str)) {
// 当文件内容字符串里面没有 {{}} 时,直接跳过
return next();
}

// 结合模板文件内容、元数据 完成自定义的改造渲染
render(str, metalsmithMetadata, (err, res) => {
if (err) {
err.message = `[${file}] ${err.message}`;
return next(err);
}
files[file].contents = new Buffer(res);
next();
});
},
done
);
};
}

/**
* Display template complete message.
*
* @param {String} message
* @param {Object} data
*/

function logMessage(message, data) {
if (!message) return;
render(message, data, (err, res) => {
if (err) {
console.error(
"\n Error when rendering template complete message: " +
err.message.trim()
);
} else {
console.log(
"\n" +
res
.split(/\r?\n/g)
.map((line) => " " + line)
.join("\n")
);
}
});
}

流程图

扩展知识

语义化版本号

语义化版本号:Semantic Versioning,简称 SemVer,是一种版本控制规范,它定义了版本号的格式和版本间的兼容性规则。

SemVer 版本号通常由三部分组成:主版本号、次版本号和补丁版本号,格式为MAJOR.MINOR.PATCH

semver 库提供了一系列函数和方法来比较、解析、验证和操作 SemVer 版本号。

面试点

命令配置

主命令:在package.jsonbin配置

1
2
3
4
5
6
7
8
{
"name": "vue-cli",
// ....

"bin": {
"vue-init": "./bin/vue-init.js"
}
}

副命令:在对应的 JS 代码里面通过commander 库实现

/bin/vue-init.js代码

1
2
3
4
5
6
#! /usr/bin/env node
const program = require('commander')
program.version('1.0')
.usage("<template-name> [project-name]")
.option('-c, --clone', 'use git clone"')
.parse(program.argv)

vue-init vue-template vue2Project --clone:将进行初始化项目

命令行参数获取

命令行输入:vue-init vue-template vue2Project --clone

  • option定义命令行参数
    • 可以通过program.key获取
    • 比如:
      • program.clone返回为boolean 的 true,判断是否输入了clone
  • option定义命令行参数
    • 可以通过program.args[index]获取,index为数组下标
    • 比如:
      • program.args[0]返回vue-init后的第一个,即vue-template
      • program.args[1]返回vue-init后的第二个,即vue2Project

命令行交互

可以通过inquirer 库实现命令行的各种问询、选择等交互

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const inquirer = require('inquirer')


const promatList = [
{
type: 'input', // 输入类型的交互
messgae: '项目名称', // 提示信息
name: 'name', // 输入的值存的 key
default: 'project' // 默认值
},
{
type: 'list', // 单选选择类型的交互
messgae: '构建工具', // 提示信息
name: 'name', // 输入的值存的 key
choices: ['cli2', 'cli3'], // 可选项
default: 'cli2' // 默认值
}
]
inquirer.promat(promatList) // 进行交互
.then(answerObj => {
// answerObj:返回用户输入的答案对象,其中的 key 为 promatList 里面的 name
})

脚手架版本检测

1
2
3
4
5
6
7
8
// 脚手架与 nodejs 的版本检测,一般用满足(类似于>=)
const semver = reqiure('semver')

const baseNodejsVersion = ">=6.0"

if(!semver.satisfies(process.version, baseNodejsVersion)) {
consoe.log('当前 nodejs 的版本不满足该脚手架,最低版本为' + baseNodejsVersion)
}

3-3、Vue cli 详解
https://mrhzq.github.io/职业上一二事/前端面试/前端八股文/3-3、Vue cli 详解/
作者
黄智强
发布于
2024年1月13日
许可协议