原文链接(格式更好):《3-3 Vue cli 详解》
前端脚手架:快速创建项目的基础代码框架和配置的工具
Vue cli 目前处于维护状态:
使用介绍:介绍 | Vue CLI
Vue3.0 推荐使用 create-vue 来创建项目
Vue cli 使用流程
创建项目 => 选择配置 => 安装依赖
// 创建项目
$ 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代码:
{
// ...
"bin": {
"vue": "bin/vue", // 这里将 vue 注入到全局
"vue-init": "bin/vue-init",
"vue-list": "bin/vue-list"
},
}
所以我们才可以直接用vue命令,比如vue -h、vue -V
其次bin/vue里面定义了更多的子命令,代码如下:
#!/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代码解析:
#!/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里面
对应代码如下:
#!/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 的版本检测代码如下:
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对应代码如下:
#!/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文件里面是最核心的生成逻辑,代码如下:
// 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.json的bin配置
{
"name": "vue-cli",
// ....
"bin": {
"vue-init": "./bin/vue-init.js"
}
}
副命令:在对应的 JS 代码里面通过commander 库实现
/bin/vue-init.js代码
#! /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-templateprogram.args[1]返回vue-init后的第二个,即vue2Project
-
命令行交互
可以通过inquirer 库实现命令行的各种问询、选择等交互
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
})
脚手架版本检测
// 脚手架与 nodejs 的版本检测,一般用满足(类似于>=)
const semver = reqiure('semver')
const baseNodejsVersion = ">=6.0"
if(!semver.satisfies(process.version, baseNodejsVersion)) {
consoe.log('当前 nodejs 的版本不满足该脚手架,最低版本为' + baseNodejsVersion)
}