橙某人

V1

2022/09/19阅读:10主题:橙心

硬刚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', { loosetrue })); // 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({
  num1,
  str'2'
}, obj);
if(res.error) {
  console.log('验证不通过')
}

console.log(res)

虽然 joi 模块的使用是傻瓜式的,但是它确实是一个非常棒的检验器,不止类型校验,它还能校验长度、时间戳、必填、唯一等等,甚至还能使用正则,这能整的活就多了。上面只是写了一个最简单例子,小伙伴们大致先有个印象即可。

image.png
image.png

cmd窗口实现清屏效果-readline

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

2.gif
2.gif

那么我们如何来实现这个清屏效果呢?我们先直接来看代码:

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, 00); 
    readline.clearScreenDown(process.stdout);
    if (title) {
      console.log(title)
    }
  }
}

clearConsole('清屏结束:橙某人');

因为涉及到的更多的是一些 Node 方面的知识,这里就不做过多的阐述了,对应变量和方法小编都有贴上链接,对 Node 感兴趣的小伙伴可以再去细究细究其中的原委。

这个方法来源于 vue-cli 的工具包 @vue/cli-shared-utilslogger.js 文件。

image.png
image.png

问答开启前清屏

上一篇 文章我们已经完成了所有问答题目的构建,并借用 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 源码后的样子,详细逻辑会在后续补充完整,你也可以提前看看 传送门

其核心大概就是会返回两个版本号信息:

  1. package.json 文件中读取 version 属性作为脚手架当前的版本号。
  2. 还有就是读取 .vuerc 文件中的 latestVersion 属性作为实际使用的脚手架版本号。

如同下图,juejin-vue-cli 脚手架小编在 package.json 文件中定义的版本号是 3.12.1,但小编实际电脑上使用的 vue-cli 脚手架版本是 5.0.4
(小编电脑上的 juejin-vue-clivue-cli 共用电脑上的一个 .vuerc 文件)

image.png
image.png

这样一个简单的清屏功能就做完啦,你可以执行 juejin-vue-cli create gg 命令试试看。

获取preset参数

上一篇 文章中,在最后我们已经获取到用户选择的粗略 preset,我们接着来把它转换成一个标准的 preset

粗略preset image.png 标准preset: image.png

回到 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 是否还有印象?我们在 第二篇 文章的时候有提及过的。

81893FE8.jpg
81893FE8.jpg

校验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 模块。

image.png
image.png

储存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, null2))
  } catch (e) {
    error('.vuerc文件写入失败')
  }
}

存储过程就比较简单的,先读取,然后合并,最后借用 fs 模块直接写入文件即可。

需要下载 lodash 的深拷贝方法:

npm install lodash.clonedeep@4.5.0

可选参数的使用

在 第二篇 文章中,我们给我们的 juejin-vue-cli 脚手架定义了几个 可选参数 一直没有使用,现在我们来看看如何使用它们。

image.png
image.png

继续回到 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 参数的信息来源就一共有四种形式:

  1. 从电脑中的 .vuerc 文件中读取出 preset
  2. 使用 vue-cli 脚手架项目中的 options.js 文件里面默认的 preset
  3. 通过可选参数 -p 读取本地或者远程项目中的 xxx.json 文件。
  4. 通过可选参数 -i {...} 输入一个 JSON 数据。

这篇文章的主题就是围绕这些方式确定最终的一个 preset 参数信息

最后,放一个运行的截图,本章就讲到这里啦。

image.png
image.png



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

image.png
image.png

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

分类:

前端

标签:

Vue.js

作者介绍

橙某人
V1

打杂工 | 广州郊区建筑工地