写在开头
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.json
,index.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
配置文件中的 preset
与 vue-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
文件的虽然写得比较复杂,但是它的作用很简单,就是用来格式化名称的,为了更好的展示效果。之所以需要格式化名称,你可以看看下图,当我们选择安装 babel
、vue-router
与 vuex
等时,在 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-router
与 vuex
的模板是放置在 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-utils
的 env.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.js
和 vuex.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