写在开头
前天刚发布 VueCli3系列 的第三篇文章,今天马不停蹄,小编又来写第四篇文章了,都卷起来。(✪ω✪)
话不多说,我们赶紧来开始本章的内容。
预备知识
semver模块
semver 模块是一个专门处理与版本相关的工具包,它的全称是 Semantic Version
,译为语义化版本的工具。
安装:
npm install semver
看不懂概念?没关系,来看下面的例子:
const semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.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
文件中:
...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.jsconst {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 semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.log(semver.cmp('1.2.3', '===' , '1.2.4'));console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));0
上面增加了三个方法用于校验 preset
参数,主要还是依赖于 vue-cli
的工具包 @vue/cli-shared-utils。
源码中其实也是借用了 joi 模块。
储存preset参数
preset
参数通过了校验后,就能把到写入 .vuerc
文件中存储起来了,以备后续的使用。
那么我们来看看如何把 preset
参数写入 .vuerc
文件中:
const semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.log(semver.cmp('1.2.3', '===' , '1.2.4'));console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));1
还是一样回到 options.js
文件,反正和 preset
参数相关的一切操作,都写在 options.js
文件中。
const semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.log(semver.cmp('1.2.3', '===' , '1.2.4'));console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));2
存储过程就比较简单的,先读取,然后合并,最后借用 fs
模块直接写入文件即可。
需要下载 lodash
的深拷贝方法:
const semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.log(semver.cmp('1.2.3', '===' , '1.2.4'));console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));3
可选参数的使用
在 第二篇 文章中,我们给我们的 juejin-vue-cli
脚手架定义了几个 可选参数 一直没有使用,现在我们来看看如何使用它们。
继续回到 Creator.js
文件中,这次要看回 create()
方法中:
const semver = require('semver');// 验证某个版本号是否合法console.log(semver.valid('1.2.3')); // 1.2.3console.log(semver.valid('a.b.c')); // nullconsole.log('***************')// 提取版本号console.log(semver.clean('=v1.2.3')); // 1.2.3console.log(semver.clean(' =v1.2.3 ')); // 1.2.3console.log(semver.clean('=v 1.2.3')); // nullconsole.log(semver.clean('= v1.2.3')); // nullconsole.log(semver.clean(' = v 1.2.3')); // nullconsole.log(semver.clean('=v1.2.3foo')); // nullconsole.log(semver.clean('=v1.2.3foo', { loose: true })); // 1.2.3-fooconsole.log(semver.clean('~v1.2.3')); // nullconsole.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')); // falseconsole.log(semver.cmp('1.2.3', '===' , '1.2.4'));console.log(semver.cmp('1.2.3', '!==' , '1.2.4'));4
从代码中可以看到,vue-cli
提供了三个可选参数来注入 preset
参数。
那么,现在 preset
参数的信息来源就一共有四种形式:
从电脑中的 .vuerc
文件中读取出 preset
。
使用 vue-cli
脚手架项目中的 options.js
文件里面默认的 preset
。
通过可选参数 -p
读取本地或者远程项目中的 xxx.json
文件。
通过可选参数 -i {...}
输入一个 JSON
数据。
这篇文章的主题就是围绕这些方式确定最终的一个 preset
参数信息。
最后,放一个运行的截图,本章就讲到这里啦。
至此,本篇文章就写完啦,撒花撒花。
希望本文对你有所帮助,如有任何疑问,期待你的留言哦。 老样子,点赞+评论=你会了,收藏=你精通了。
原文:https://juejin.cn/post/7097140169333014558