首页>>前端>>Vue->硬刚VueCli3源码系列四

硬刚VueCli3源码系列四

时间:2023-11-30 本站 点击:1

写在开头

前天刚发布 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-clivue-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


本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若转载,请注明出处:/Vue/3771.html