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

硬刚VueCli3源码系列三

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

写在开头

VueCli3源码系列 又开始更新啦,最近小编更新文章的频率有点下降了,也不知道跑了多少读者(ಥ_ಥ) ,可能也根本没啥读者吧,哈哈哈( ̄▽ ̄)~,不过想来也正常,源码系列都比较枯燥,能坚持的不多,小编也不求写的源码系列文章能有多少阅读量,反正就当写着玩吧,也希望偶尔能给某些有需求的小伙伴一些帮助。

然后回顾一下,为什么更新频率下降的原因,一方面是写源码系列文章还是有点压力的,很多时候小编自己也看得焦头烂额,更别提写文章了;而另一方面就是从三月份开始就忙于跳槽事宜,各种刷题,如面经、力扣等等,前前后后两个月时间整理了大概两万字的总结文档,后续也会把这些面经给整理出来。(✪ω✪)

当然,从三月份初到 2022-05-05 这天,工作的事情总算是尘埃落定了,后续如果工作上不忙的话,会加速更新完这系列文章。

预备知识

chalk模块

chalk 模块是一个可以美化 console.log 输出语句的包,能非常方便的输出多种颜色的 log,包括修改字体颜色,加粗字体,改变字体背景色等等。

这个模块的使用非常简单,我们直接来看一下例子就行。

安装:

npm install chalk

使用:

const chalk = require('chalk');console.log(chalk.yellow('我是黄色字体'));console.log(chalk.blue('我是蓝色字体'));console.log(chalk.bgRed('红色背景'));console.log(chalk.red.bold('红色加粗字体'));console.log(chalk.underline('下划线'));

认识.vuerc文件

不知道你是否有这么一个疑问?当我们使用 vue create myProject 命令来快速初始化一个项目时,cmd 控制台会有一些交互式的选择,如下:

当我们选择 Manually select features 后,你就可以手动选择项目中需要安装的插件了。而当我们完成一系列的选择后,在最后 vue-cli 会询问我们是否需要保存此次操作结果,如果你选择了 Yes 则还会让你输入"名称"。

而当我们下次再次执行 vue create myProject2 命令初始化项目时,我们会看到多了一个上次选择结果的选项供我们选择。

那么,知道这么一个操作过程后,我们回到上面讲的疑问?

这个疑问就是 vue-cli 是如何存储我们之前选择的结果呢?它会存在什么地方呢?以什么形式储存呢?这个疑问在 vue-cli 文档上有大致的介绍,但我猜应该大部分人还不清楚,或者看过但却不知道实际的意义。

其实这里 vue-cli 会使用到一个 .vuerc 配置文件,它是自动创建的,存在于你系统下的 home 目录内(window系统在 C:\Users\当前登陆用户\.vuerc)。

打开这个文件后,你可以看到在 presets 属性下,会储存你之前选择的结果信息,这个文件还储存其他一些配置信息,如是否使用 cnpm 镜像,还有版本号等等信息。

注意这个presets,它非常重要,后续会围绕如何获取它来展开逻辑。

构建交互式问答题目列表

了解完 .vuerc 配置文件的作用后,我们就可以来继续完善 上一篇 文章写的 Creator.js 核心文件的逻辑了。

这里小编先大致讲一下 Creator.js 文件核心做的事情:

构建交互问答题目列表;

然后获取用户交互式选择后的结果,也就是 preset 参数的信息;

然后根据 preset 信息去安装(npm install)相关的模块;

最后去创建一些必要的模板文件,如 package.jsonindex.html 等等。

接下来,文章的主题是完成第一步和第二步的过程,我们需要先来构建相关的各种问答题目,然后获取用户选择问答后的结果 preset 信息。

构建第一和第二个问答题目

首先,我们先来看看如何构建第一和第二个交互问答题目的,这里还是会使用到 inquirer 模块,对它还不熟悉的小伙伴私下要努力努力了。

// Creator.jsconst {defaults, loadOptions} = require('./options');const {formatFeatures} = require('./util/features');const isManualMode = answers => answers.preset === '__manual__'; // 是否选择手动交互module.exports = class Creator {   constructor (name, context, promptModules) {    this.name = name; // 项目名    this.context = context; // 项目路径    const { presetPrompt, featurePrompt } = this.resolveIntroPrompts(); // 第一,第二个问答    this.presetPrompt = presetPrompt;     this.featurePrompt = featurePrompt;   }   async create(cliOptions = {}, preset = null) {}  getPresets() {    const savedOptions = loadOptions(); // 读取 .vuerc 文件的内容, 里面会保留 "上次选择的结果" 的数据    return Object.assign({}, savedOptions.presets, defaults.presets); // defaults.presets 默认  }  resolveIntroPrompts() {    const presets = this.getPresets();    // 格式化插件名称,为了更好的展示效果: @vue/cli-plugin-babel -> babel  or  @vue/cli-plugin-eslint -> eslint    const presetChoices = Object.keys(presets).map(name => {      return {        name: `${name} (${formatFeatures(presets[name])})`,        value: name      }    });    // 第一个问答:选择默认安装还是手动选择安装    const presetPrompt = {      name: 'preset',      type: 'list',      message: `请选择安装的交互模式:`,      choices: [        ...presetChoices, // .vuerc的preset与vue-cli默认的preset        {          name: '手动选择',          value: '__manual__'        }      ]    }    // 第二个问答: 选择需要安装的插件列表    const featurePrompt = {      name: 'features',      when: isManualMode, // 当选择了手动安装      type: 'checkbox',      message: '请选择需要安装的插件,空格控制是否勾选:',      choices: [], // 插件列表, 暂时为空, 后面会在constructor中注入      pageSize: 10    }    return {      presetPrompt,      featurePrompt    }  }}

上面代码中,小编添加了几个新方法,从 constructor() 入口出发,我们先看到resolveIntroPrompts() 方法,它用于构建第一和第二个交互问答信息。

不过在具体构建问答之前,我们应该先去读取 .vuerc 配置文件,看看有没有之前的操作结果保存下来,所以我们在 resolveIntroPrompts() 方法开头就去调用了 getPresets() 方法,这个方法会把 .vuerc 配置文件中的 presetvue-cli 默认的 preset 合并然后返回。

之所以要先获取 preset 是因为我们第一个问答题目会使用到它,可以看看如下图:

创建 options.js 文件:

const fs = require('fs');const { error } = require('@vue/cli-shared-utils/lib/logger');const { getRcPath } = require('./util/rcPath');const rcPath = getRcPath('.vuerc'); // 它存在于你的 C:\Users\当前登陆用户\.vuerclet cachedOptions;exports.loadOptions = () => {  // 如果有缓存则直接返回  if (cachedOptions) return cachedOptions;  if (fs.existsSync(rcPath)) {    try {      // 使用同步方式读取系统中的 .vuerc 文件出来      cachedOptions = JSON.parse(fs.readFileSync(rcPath, 'utf-8'));    } catch (e) {      error('文件读取失败'); // vue-cli工具包中对console.error的封装, 并会上报相关错误      exit(1); // 退出    }    return cachedOptions;   } else {     return {}; // 没有则返回一个空对象   }};// 一个标准的preset形式, 默认安装时, preset就等于它exports.defaultPreset = {  router: false, // 是否安装router  vuex: false, // 是否安装vuex  useConfigFiles: false, // 是否使用独立的配置文件  cssPreprocessor: undefined, // css预处理器配置  plugins: { // 插件相关, 默认安装会自动安装babel和eslint    '@vue/cli-plugin-babel': {},    '@vue/cli-plugin-eslint': {      config: 'base',      lintOn: ['save']    }  }}exports.defaults = {  lastChecked: undefined, // 是否检查版本  latestVersion: undefined, // 版本号  packageManager: undefined, // 选择安装依赖时的下载源  useTaobaoRegistry: undefined, // 是否使用cnpm  presets: {    'default': exports.defaultPreset  }}

新建 ./util/rcPath.js 文件:

const os = require("os");const path = require("path");exports.getRcPath = (file) => {   // os.homedir() 能直接获取到系统当前用户的路径   return path.join(os.homedir(), file);};

getPresets() 方法主要是通过 options.js 文件和 rcPath.js 文件来配合完成的,loadOptions() 方法能帮助我们直接读取到 .vuerc 文件的内容并且序列化。

Creator.js 文件中,我们还引入 ./util/features.js 文件:

const chalk = require('chalk'); // 记得安装 chalk 模块:npm install chalk@2.4.1const { toShortPluginId } = require('@vue/cli-shared-utils');exports.getFeatures = (preset) => {  const features = []  if (preset.router) {    features.push('vue-router')  }  if (preset.vuex) {    features.push('vuex')  }  if (preset.cssPreprocessor) {    features.push(preset.cssPreprocessor)  }  const plugins = Object.keys(preset.plugins).filter(dep => {    return dep !== '@vue/cli-service'  })  features.push.apply(features, plugins)  return features}// 返回: (\x1B[33mbabel\x1B[39m, \x1B[33meslint\x1B[39m) exports.formatFeatures = (preset, lead, joiner) => {  const features = exports.getFeatures(preset)  return features.map(dep => {    dep = toShortPluginId(dep)    return `${lead || ''}${chalk.yellow(dep)}`  }).join(joiner || ', ')}

./util/features.js 文件的虽然写得比较复杂,但是它的作用很简单,就是用来格式化名称的,为了更好的展示效果。之所以需要格式化名称,你可以看看下图,当我们选择安装 babelvue-routervuex 等时,在 vue-cli 中用 preset 来存储是以图下(@vue/cli-plugin-bale)全称这种形式的:

但是,展示给用户的是插件的简写形式:

到这里你可能会问,为什么在 preset 中是以 @vue/cli-plugin-插件名称 来存储呢?用户选择了 babel 直接就是 babel: true 不就可以了嘛?当然没那么简单啦。(✪ω✪)  这是因为当我们选择安装 vuex 插件时,我们不仅需要安装 vuex 这个包,还需要为项目生成一些 vuex 的基础模板,如 store/index.js 文件等等。而这些模板 vue-cli 都给每个插件单独写了一个包存储,如下: 后面我们会根据 @vue/cli-plugin-插件名称 形式,把需要的包加载过来,获取到里面的模板信息。

上面小编截图使用的 vue-cli 版本是5.0.1,但是在3.x.x版本中,vue-routervuex 的模板是放置在 cli-service 包中的。

构建其他问答题目

我们接着来看看其他问答题目构建,比较简单,我们就直接来看代码了:

// Creator.js...const {hasYarn, hasPnpm3OrLater} = require('@vue/cli-shared-utils');const isManualMode = answers => answers.preset === '__manual__'; module.exports = class Creator {   constructor (name, context, promptModules) {    ...    this.outroPrompts = this.resolveOutroPrompts(); // 其他问答  }   async create(cliOptions = {}, preset = null) {}  getPresets() { ... }  resolveIntroPrompts() { ... }  resolveOutroPrompts() {    const outroPrompts = [      // 对于eslint/postcss是否使用独立的配置文件      {        name: 'useConfigFiles',        when: isManualMode,        type: 'list',        message: 'Where do you prefer placing config for Babel, PostCSS, ESLint, etc.?',        choices: [          {            name: 'In dedicated config files',            value: 'files'          },          {            name: 'In package.json',            value: 'pkg'          }        ]      },      {        name: 'save',        when: isManualMode,        type: 'confirm',        message: '此次操作是否存储起来?',        default: false      },      {        name: 'saveName',        when: answers => answers.save,        type: 'input',        message: '请输入此次操作存储起来的名称:'      }    ]    // 检测如果有 yarn/pnpm 等其他源, 会可以选择下载源题目    const savedOptions = loadOptions()    if (!savedOptions.packageManager && (hasYarn() || hasPnpm3OrLater())) {      const packageManagerChoices = []      if (hasYarn()) {        packageManagerChoices.push({          name: 'Use Yarn',          value: 'yarn',          short: 'Yarn'        })      }      if (hasPnpm3OrLater()) {        packageManagerChoices.push({          name: 'Use PNPM',          value: 'pnpm',          short: 'PNPM'        })      }      packageManagerChoices.push({        name: 'Use NPM',        value: 'npm',        short: 'NPM'      })      outroPrompts.push({        name: 'packageManager',        type: 'list',        message: 'Pick the package manager to use when installing dependencies:',        choices: packageManagerChoices      })    }    return outroPrompts  }}

resolveOutroPrompts() 方法主要用于构建"是否使用独立配置文件"、"是否保存此次选择结果"与"选择下载源"这三个题目。其中还使用到了检测下载源 hasYarn()hasPnpm3OrLater() 方法,这两个方法来源于 vue-cli 工具包 @vue/cli-shared-utilsenv.js 文件,感兴趣的小伙伴可以再去细看其中的实现逻辑。

更多的问答题目

上面我们大概构建了四五个问答题目,但这远远还没完呢,还有很多插件选择后的后续交互问答题目呢。比如,选择了 vue-router 插件,后续会继续询问你 vue-router 的模式是否使用 history 模式。

我们接着来看,还是在 constructor() 中做修改:

// Creator.js...const PromptModuleAPI = require('./PromptModuleAPI');const isManualMode = answers => answers.preset === '__manual__'; module.exports = class Creator {   constructor (name, context, promptModules) {    ...    // 这三个存放的值来自于 ProptModuleAPI.js 文件 或者 promptModules 文件夹下的文件    this.injectedPrompts = []; // 存储插件的后续问答题目, 如vue-router是否使用history模式    this.promptCompleteCbs = []; // 存储插件的后续问题的 回调函数, 如选择了vue-router使用history模式, 会往其中放入一个回调, 这个回调会对用户的preset添加一个router: true    // 这两句代码主要功能是完善第二个问答缺失的 手动安装的选择列表 信息    const promptAPI = new PromptModuleAPI(this);    // 遍历每一个文件, 每个文件的方法接收一个 promptAPI 实例, 实例上有各种操作方法    promptModules.forEach(m => m(promptAPI)); // 执行 m() 方法  }   async create(cliOptions = {}, preset = null) {}  getPresets() { ... }  resolveIntroPrompts() { ... }  resolveOutroPrompts() { ... }}

新建 PromptModuleAPI.js 文件:

module.exports = class PromptModuleAPI {  constructor (creator) {    this.creator = creator  }  // 往 第二道题目 添加选项  injectFeature (feature) {    this.creator.featurePrompt.choices.push(feature)  }  // 往injectedPrompts容器添加 插件的后续问答题目  injectPrompt (prompt) {    this.creator.injectedPrompts.push(prompt)  }  // 往injectedPrompts容器添加 插件的后续问答题目, 如果题目又是一个 列表 选择的话  injectOptionForPrompt (name, option) {    this.creator.injectedPrompts.find(f => {      return f.name === name    }).choices.push(option)  }  // 往promptCompleteCbs容器添加回调函数, 如使用vue-router的history模式, 那么后续要在用户的preset中作一个标识  onPromptComplete (cb) {    this.creator.promptCompleteCbs.push(cb)  }}

上面新增加的代码不多,但是会比较绕,需要你细细去体会。对于 constructor() 方法中的第三个参数 promptModules 我们在上一篇文章有大致提及了一下,当时还新建了 router.jsvuex.js 两个空文件,这次我们来把它们补全。

// router.jsconst chalk = require('chalk')module.exports = cli => {  cli.injectFeature({    name: 'Router',    value: 'router',    description: 'Structure the app with dynamic pages',    link: 'https://router.vuejs.org/'  })  cli.injectPrompt({    name: 'routerHistoryMode',    when: answers => answers.features.includes('router'), // answers为交互答题后的结果    type: 'confirm',    message: `Use history mode for router? ${chalk.yellow(`(Requires proper server setup for index fallback in production)`)}`,    description: `By using the HTML5 History API, the URLs don't need the '#' character anymore.`,    link: 'https://router.vuejs.org/guide/essentials/history-mode.html'  })  cli.onPromptComplete((answers, options) => {    if (answers.features.includes('router')) {      options.router = true      options.routerHistoryMode = answers.routerHistoryMode    }  })}

const chalk = require('chalk');console.log(chalk.yellow('我是黄色字体'));console.log(chalk.blue('我是蓝色字体'));console.log(chalk.bgRed('红色背景'));console.log(chalk.red.bold('红色加粗字体'));console.log(chalk.underline('下划线'));0

补全后,我们就总算是把所有题目都构建完成了。小编估计小伙伴们看到这里可能都晕掉了。(写的是啥啊?拉出来打)

运行交互问答题目

不要着急,虽然前面写了那么多铺垫,但都比较虚,下面小编让它跑起来,看看实际的效果会更生动一些。

回到 Creator.js 文件:

const chalk = require('chalk');console.log(chalk.yellow('我是黄色字体'));console.log(chalk.blue('我是蓝色字体'));console.log(chalk.bgRed('红色背景'));console.log(chalk.red.bold('红色加粗字体'));console.log(chalk.underline('下划线'));1

注意,前面我们构建题目都只是在 constructor 中初始化完成,而文件中的 create() 方法才是我们的入口核心方法。

下面我们执行 juejin-vue-cli create gg 命令:

这样我们就把问答的交互运行起来了,接下来我们只要根据选择的结果做后续的逻辑就可以了。

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

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

原文:https://juejin.cn/post/7096319924989067271


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