
橙某人
2022/09/19阅读:37主题:橙心
硬刚VueCli3源码系列四 - cmd窗口清屏、获取/校验/储存preset参数、可选参数的使用
写在开头
前天刚发布 VueCli3系列 的第三篇文章,今天马不停蹄,小编又来写第四篇文章了,都卷起来。(✪ω✪)
话不多说,我们赶紧来开始本章的内容。
预备知识
semver模块
semver 模块是一个专门处理与版本相关的工具包,它的全称是 Semantic Version
,译为语义化版本的工具。
安装:
npm install semver
看不懂概念?没关系,来看下面的例子:
const semver = require('semver');
// 验证某个版本号是否合法
console.log(semver.valid('1.2.3')); // 1.2.3
console.log(semver.valid('a.b.c')); // null
console.log('***************')
// 提取版本号
console.log(semver.clean('=v1.2.3')); // 1.2.3
console.log(semver.clean(' =v1.2.3 ')); // 1.2.3
console.log(semver.clean('=v 1.2.3')); // null
console.log(semver.clean('= v1.2.3')); // null
console.log(semver.clean(' = v 1.2.3')); // null
console.log(semver.clean('=v1.2.3foo')); // null
console.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-foo
console.log(semver.clean('~v1.2.3')); // null
console.log('***************')
// 比较两个版本号的大小
console.log(semver.gt('1.2.3', '1.2.4')); // v1 > v2; false;
console.log(semver.lt('1.2.3', '1.2.4')); // v1 < v2; true;
console.log(semver.gte('1.2.3', '1.2.4')); // v1 >= v2; false;
console.log(semver.lte('1.2.3', '1.2.4')); // v1 <= v2; true;
console.log(semver.eq('1.2.3', '1.2.4')); // v1 == v2; false;
console.log(semver.neq('1.2.3', '1.2.4')); // v1 != v2; true;
console.log(semver.cmp('1.2.3', '>' , '1.2.4')); // false
console.log(semver.cmp('1.2.3', '===' , '1.2.4'));
console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));
joi模块
joi 模块是一个用于校验 JS
对象的包,能非常简单就能完成对象属性的校验,需要注意的是它在版本上所拥有的方法是有区别的。文档
安装:
npm install joi@14.3.1
示例:
const joi = require('joi');
let obj = {
num: joi.number(),
str: joi.string()
}
let res = joi.validate({
num: 1,
str: '2'
}, obj);
if(res.error) {
console.log('验证不通过')
}
console.log(res)
虽然 joi
模块的使用是傻瓜式的,但是它确实是一个非常棒的检验器,不止类型校验,它还能校验长度、时间戳、必填、唯一等等,甚至还能使用正则,这能整的活就多了。上面只是写了一个最简单例子,小伙伴们大致先有个印象即可。

cmd窗口实现清屏效果-readline
cmd
窗口清屏是什么呢?可以看看如下的 gif
图,大概就是在输入某行命令后,后续的内容能直接从 cmd
窗口的头部开始展示,方便我们查看。

那么我们如何来实现这个清屏效果呢?我们先直接来看代码:
const readline = require('readline')
function clearConsole(title) {
if (process.stdout.isTTY) {
const blank = '\n'.repeat(process.stdout.rows); // '\n\n\n...'
console.log(blank);
readline.cursorTo(process.stdout, 0, 0);
readline.clearScreenDown(process.stdout);
if (title) {
console.log(title)
}
}
}
clearConsole('清屏结束:橙某人');
-
process.stdout.isTTY:判断是否连接到 TTY 上下文。 -
process.stdout.rows:获取cmd窗口的行数。 -
readline.cursorTo():将光标移动到给定的 TTY stream 中的指定位置。 -
readline.clearScreenDown():从光标的当前位置向下清除给定的 TTY 流。
因为涉及到的更多的是一些 Node
方面的知识,这里就不做过多的阐述了,对应变量和方法小编都有贴上链接,对 Node
感兴趣的小伙伴可以再去细究细究其中的原委。
这个方法来源于 vue-cli
的工具包 @vue/cli-shared-utils 的 logger.js 文件。

问答开启前清屏
上一篇 文章我们已经完成了所有问答题目的构建,并借用 inquirer 模块开启了实际的问答过程。但是在开启问答之前,我们应该先清理一下 cmd
窗口,这样用户拥有更好的用户体验。
// Creator.js
...
const {clearConsole} = require('./util/clearConsole');
module.exports = class Creator {
constructor (name, context, promptModules) { ... }
async create(cliOptions = {}, preset = null) {
preset = await this.promptAndResolvePreset();
}
async promptAndResolvePreset (answers = null) {
if (!answers) {
// 问答开启前, 这句代码不会影响正常流程
await clearConsole(true);
// 开启问答
answers = await inquirer.prompt(this.resolveFinalPrompts());
}
}
resolveFinalPrompts () { ... }
resolveIntroPrompts() { ... }
getPresets () { ... }
resolveIntroPrompts () { ... }
resolveOutroPrompts () { ... }
}
新建 clearConsole.js
文件:
const chalk = require('chalk');
const semver = require('semver');
const { clearConsole } = require('@vue/cli-shared-utils');
const getVersions = require('./getVersions');
exports.generateTitle = async function (checkUpdate) {
// 获取脚手架当前的版本号和最新版本号
const { current, latest } = await getVersions();
// 下面所有的逻辑就是为了绘制一个在cmd窗口上展示的文案效果
let title = chalk.bold.blue(`Vue CLI v${current}`);
if (process.env.VUE_CLI_TEST) {
title += ' ' + chalk.blue.bold('TEST')
}
if (process.env.VUE_CLI_DEBUG) {
title += ' ' + chalk.magenta.bold('DEBUG')
}
if (checkUpdate && semver.gt(latest, current)) { // gt: v1>v2
if (process.env.VUE_CLI_API_MODE) {
title += chalk.green(` 🌟️ Update available: ${latest}`)
} else {
title += chalk.green(`
┌────────────────────${`─`.repeat(latest.length)}──┐
│ Update available: ${latest} │
└────────────────────${`─`.repeat(latest.length)}──┘`)
}
}
return title;
}
exports.clearConsole = async function clearConsoleWithTitle (checkUpdate) {
const title = await exports.generateTitle(checkUpdate); // 获取清屏后的展示文案
clearConsole(title); // 清屏
}
注意这里需要安装 semver
模块哦,npm install semver@6.0.0
。
新建 getVersions.js
文件:
const { loadOptions } = require('../options');
let sessionCached;
/**
* @returns {
* current: 当前版本号, 来源于package.json
* latest: 最新版本号, 在 .vuerc 文件中的 latestVersion 属性会存储最新的版本号信息
* }
*/
module.exports = async function getVersions () {
// 版本信息有缓存则直接返回
if (sessionCached) return sessionCached;
const local = require(`../../package.json`).version; // 读取 package.json 文件中的版本号
const latest = loadOptions().latestVersion; // 读取 .vuerc 文件中的版本号
return (sessionCached = {
current: local,
latest
})
}
上面展示 getVersions.js
文件的代码是小编精简 vue-cli
源码后的样子,详细逻辑会在后续补充完整,你也可以提前看看 传送门。
其核心大概就是会返回两个版本号信息:
-
从 package.json
文件中读取version
属性作为脚手架当前的版本号。 -
还有就是读取 .vuerc
文件中的latestVersion
属性作为实际使用的脚手架版本号。
如同下图,juejin-vue-cli
脚手架小编在 package.json
文件中定义的版本号是 3.12.1
,但小编实际电脑上使用的 vue-cli
脚手架版本是 5.0.4
。
(小编电脑上的 juejin-vue-cli
与 vue-cli
共用电脑上的一个 .vuerc
文件)

这样一个简单的清屏功能就做完啦,你可以执行 juejin-vue-cli create gg
命令试试看。
获取preset参数
在 上一篇 文章中,在最后我们已经获取到用户选择的粗略 preset
,我们接着来把它转换成一个标准的 preset
。
粗略preset 标准preset:
回到 Creator.js
文件中:
...
const path = require('path');
module.exports = class Creator {
constructor (name, context, promptModules) { ... }
async create(cliOptions = {}, preset = null) {
preset = await this.promptAndResolvePreset();
}
async promptAndResolvePreset (answers = null) {
if (!answers) {
await clearConsole(true);
answers = await inquirer.prompt(this.resolveFinalPrompts());
}
// 构建最终的 preset
let preset;
if (answers.preset && answers.preset !== '__manual__') { // 默认 或者 上次选择结果
preset = await this.resolvePreset(answers.preset);
} else { // 手动
preset = {
useConfigFiles: answers.useConfigFiles === 'files',
plugins: {}
}
answers.features = answers.features || [];
// promptCompleteCbs容器我们上一篇文章有讲过, 它存放一些回调函数, 这些函数用于修改preset
this.promptCompleteCbs.forEach(cb => cb(answers, preset));
}
}
async resolvePreset (name, clone) {
let preset;
const savedPresets = loadOptions().presets || {}; // 读取 .vuerc 文件中的 presets
// name: "上一次选择结果" 保留的名称, 如 my-config
if (name in savedPresets) {
preset = savedPresets[name];
} else if (name.endsWith('.json') || /^\./.test(name) || path.isAbsolute(name)) {
// todo: 找本地
} else if (name.includes('/')) {
// todo: 找远程
}
// 直接取项目中默认的 preset
if (name === 'default' && !preset) {
preset = defaults.presets.default
}
/**
* 如果preset到这里还不存在, 说明有两种情况:
* 1. 储存在 options.js 文件中默认的preset被人为删除了
* 2. 电脑中的 .vuerc 文件有问题, 可能是人为改动了
*/
if (!preset) {
error(`preset "${name}" not found.`)
const presets = Object.keys(savedPresets)
if (presets.length) {
log('请你把.vuerc文件下的presets属性补全哦,要不俺可不让你跑下去了')
} else {
log('你的.vuerc文件中没有预设任何的preset, 你可以通过先手动选择后保存一个预设preset')
}
exit(1)
}
return preset
}
resolveFinalPrompts () { ... }
resolveIntroPrompts() { ... }
getPresets () { ... }
resolveIntroPrompts () { ... }
resolveOutroPrompts () { ... }
}
上面代码中,有两个空的 if
判断,它们是做什么用的呢?其实 文档 中也有相关的介绍,它们的主要作用是当你使用 -p
参数提供了本地或者远程项目的形式来注入 preset
,那么就会进入其中去执行相应的逻辑。
其实说白了就是执行 vue create -p (本地/远程)项目路径 projectName
命令后, vue-cli
脚手架会通过本地路径或者远程链接(会发送请求),去找到这个项目下的 xxx.json
文件,从中读取出 preset
来使用。
不知道你对可选参数 -p
是否还有印象?我们在 第二篇 文章的时候有提及过的。

校验preset参数
我们接着看下面的逻辑:
// Creator.js
const {defaults, loadOptions, validatePreset} = require('./options');
module.exports = class Creator {
constructor (name, context, promptModules) { ... }
async create(cliOptions = {}, preset = null) {
preset = await this.promptAndResolvePreset();
}
async promptAndResolvePreset (answers = null) {
...
// 验证preset格式, 形式必须和 options.js 文件中的 defaultPreset 一致
validatePreset(preset)
}
async resolvePreset (name, clone) { ... }
resolveFinalPrompts () { ... }
resolveIntroPrompts() { ... }
getPresets () { ... }
resolveIntroPrompts () { ... }
resolveOutroPrompts () { ... }
}
回到 options.js
文件:
...
const { createSchema, validate } = require('@vue/cli-shared-utils/lib/validate');
// 验证preset的形式
exports.validatePreset = preset => validate(preset, presetSchema, msg => {
error(`invalid preset options: ${msg}`)
})
// 校验exports.defaultPreset
const presetSchema = createSchema(joi => joi.object().keys({
bare: joi.boolean(),
useConfigFiles: joi.boolean(),
router: joi.boolean(),
routerHistoryMode: joi.boolean(),
vuex: joi.boolean(),
// TODO: remove 'sass' or make it equivalent to 'dart-sass' in v4
cssPreprocessor: joi.string().only(['sass', 'dart-sass', 'node-sass', 'less', 'stylus']),
plugins: joi.object().required(),
configs: joi.object()
}))
// 校验exports.defaults
const schema = createSchema(joi => joi.object().keys({
latestVersion: joi.string().regex(/^\d+\.\d+\.\d+$/),
lastChecked: joi.date().timestamp(),
packageManager: joi.string().only(['yarn', 'npm', 'pnpm']),
useTaobaoRegistry: joi.boolean(),
presets: joi.object().pattern(/^/, presetSchema)
}))
上面增加了三个方法用于校验 preset
参数,主要还是依赖于 vue-cli
的工具包 @vue/cli-shared-utils。
源码中其实也是借用了 joi 模块。

储存preset参数
preset
参数通过了校验后,就能把到写入 .vuerc
文件中存储起来了,以备后续的使用。
那么我们来看看如何把 preset
参数写入 .vuerc
文件中:
// Creator.js
...
const {defaults, loadOptions, validatePreset, savePreset} = require('./options');
module.exports = class Creator {
constructor (name, context, promptModules) { ... }
async create(cliOptions = {}, preset = null) {
preset = await this.promptAndResolvePreset();
}
async promptAndResolvePreset (answers = null) {
...
validatePreset(preset)
// 如果要保存这次选择的结果, 则进行相关处理
if (answers.save && answers.saveName) {
savePreset(answers.saveName, preset)
}
return preset
}
async resolvePreset (name, clone) { ... }
resolveFinalPrompts () { ... }
resolveIntroPrompts() { ... }
getPresets () { ... }
resolveIntroPrompts () { ... }
resolveOutroPrompts () { ... }
}
还是一样回到 options.js
文件,反正和 preset
参数相关的一切操作,都写在 options.js
文件中。
// options.js
...
const cloneDeep = require('lodash.clonedeep')
...
/**
* @param {*} name: 存储的名称
* @param {*} preset: 存储的preset
*/
exports.savePreset = (name, preset) => {
// 先读取 .vuerc 文件的配置信息, 并进行深拷贝, 防止影响缓存
const presets = cloneDeep(exports.loadOptions().presets || {});
presets[name] = preset;
// 存储
exports.saveOptions({ presets });
}
exports.saveOptions = toSave => {
// 先读取 .vuerc 文件的配置信息
const options = Object.assign(cloneDeep(exports.loadOptions()), toSave);
// 删除一些多余的字段
for (const key in options) {
if (!(key in exports.defaults)) {
delete options[key]
}
}
cachedOptions = options; // 缓存起来
try {
// 写入 .vuerc 文件中
fs.writeFileSync(rcPath, JSON.stringify(options, null, 2))
} catch (e) {
error('.vuerc文件写入失败')
}
}
存储过程就比较简单的,先读取,然后合并,最后借用 fs
模块直接写入文件即可。
需要下载 lodash
的深拷贝方法:
npm install lodash.clonedeep@4.5.0
可选参数的使用
在 第二篇 文章中,我们给我们的 juejin-vue-cli
脚手架定义了几个 可选参数 一直没有使用,现在我们来看看如何使用它们。

继续回到 Creator.js
文件中,这次要看回 create()
方法中:
// Creator.js
...
const cloneDeep = require('lodash.clonedeep');
module.exports = class Creator {
constructor (name, context, promptModules) { ... }
async create(cliOptions = {}, preset = null) {
if (!preset) {
if (cliOptions.preset) { // -p
// vue create projectName --preset project(本地或者远程项目地址)
preset = await this.resolvePreset(cliOptions.preset, cliOptions.clone)
} else if (cliOptions.default) { // -d
// vue create projectName --default
preset = defaults.presets.default
} else if (cliOptions.inlinePreset) { // -i
// 直接提供一个JSON数据
// vue create projectName --inlinePreset {...}
try {
preset = JSON.parse(cliOptions.inlinePreset);
} catch (e) {
error('提供的JSON数据格式有误');
exit(1);
}
} else {
preset = await this.promptAndResolvePreset()
}
}
// 深拷贝preset
preset = cloneDeep(preset);
console.log('preset:', preset);
}
async promptAndResolvePreset (answers = null) { ... }
async resolvePreset (name, clone) { ... }
resolveFinalPrompts () { ... }
resolveIntroPrompts() { ... }
getPresets () { ... }
resolveIntroPrompts () { ... }
resolveOutroPrompts () { ... }
}
从代码中可以看到,vue-cli
提供了三个可选参数来注入 preset
参数。
那么,现在 preset
参数的信息来源就一共有四种形式:
-
从电脑中的 .vuerc
文件中读取出preset
。 -
使用 vue-cli
脚手架项目中的options.js
文件里面默认的preset
。 -
通过可选参数 -p
读取本地或者远程项目中的xxx.json
文件。 -
通过可选参数 -i {...}
输入一个JSON
数据。
这篇文章的主题就是围绕这些方式确定最终的一个 preset
参数信息。
最后,放一个运行的截图,本章就讲到这里啦。

至此,本篇文章就写完啦,撒花撒花。

希望本文对你有所帮助,如有任何疑问,期待你的留言哦。
老样子,点赞+评论=你会了,收藏=你精通了。
作者介绍

橙某人
打杂工 | 广州郊区建筑工地