Vue Cli 主体上分为四个模块 cli、cli-service、cli-service-global、ui 和各种插件。

cli

cli 模块是 VueCLi3 的核心模块,这里封装了关键的项目构建功能(creat)即模板功能、持续构建的插件管理功能(add、inspect)、模板配置预设的 config 管理功能、以及其他高级功能的调用入口(service、ui)。

cli 通过 create 命令实现了基本的模板功能,通过 add、inspect 、invoke 提供了对模板的可持续构建。

记:构建思路

执行流程

这里的核心是 Generator + GeneratorAPI ,一个负责构建(管理插件、管理流程),一个负责模块化构建内容(插件)。

Generator

核心代码:

询问配置:

  1. //询问配置
  2. const promptApi = new PromptModuleAPI(this);
  3. promptModules.forEach((m) => m(promptApi));
  1. 例如bable配置: ```javascript module.exports = (cli) => { cli.injectFeature({ name: “Babel”, value: “babel”, short: “Babel”, description: “Transpile modern JavaScript to older versions (for compatibility)”, link: “https://babeljs.io/“, checked: true, });

    cli.onPromptComplete((answers, options) => { logInfo(‘onPromptComplete:根据选择结果,将选择结果最终合并到 options (preset) 中’) if (answers.features.includes(“ts”)) { if (!answers.useTsWithBabel) {

    1. return;

    } } else if (!answers.features.includes(“babel”)) { return; } options.plugins[“@vue/cli-plugin-babel”] = {}; }); };

  1. 2. 加载插件-》构造项目
  2. ```javascript
  3. const plugins = await this.reslovePlugins(preset.plugins);
  4. const generator = new Generator(this.context, {
  5. pkg,
  6. plugins,
  7. completeCbs: this.createCompleteCbs,
  8. });
  9. await generator.generate();
  1. 加载插件

    1. plugins.forEach(({ id, apply, options }) => {
    2. const api = new GeneratorAPI(id, this, options, rootOptions);
    3. apply(api, options, rootOptions, invoking);
    4. });

    例如调用cli-service generator 进行项目默认模板的初始化。

  2. 写入文件

    1. async generate({ extractConfigFiles = false, checkExisting = false } = {}) {
    2. logStep("4.3 generator-generate,再次保存各个配置文件");
    3. this.files["package.json"] = JSON.stringify(this.pkg, null, 2) + "\n";
    4. // write/update file tree to disk
    5. const initialFiles = Object.assign({}, this.files)
    6. await writeFileTree(this.context, this.files, initialFiles);
    7. }

    总的执行流程如下:

    1. bin/vue creat app-name
    2. -> create 构建开始、 收集输入的参数、判断是否覆盖之前的文件。
    3. -> new Creator 进入到 Creator 重开始构建过程,这里可以看到完整的构建流程
    4. 准备配置、选择配置、安装依赖
    5. -> new Generator 这里是安装npm 依赖完成后,拿到插件,主要是 @vue/cli-service 开始构建模板内容, 也会以依次执行其他插件的构建过程
    6. -> 构建的核心即 GeneratorAPI ,该函数封装了模板输出的方法,它和模板的关系,类似于 dom 和虚拟 dom 的关系,但是也包括的渲染过程。该模块也是 vue-cli 插件系统实现的核心。正因为这里开发的接口和统一的输出才能实现插件系统。

    记:依赖

    chalk

    样式更丰富的 log 日志插件,用例: ```javascript const chalk = require(“chalk”);

console.log(chalk.blue(“-> “ + msg));

  1. <a name="vN7aZ"></a>
  2. ### [didyoumean](https://www.npmjs.com/package/didyoumean)
  3. 匹配输入可能对应的结果,这个可能可以是准确值,也可以是相似值,默认近似比例 0.4,可以配置近似程度。
  4. ```javascript
  5. var input = 'insargrm'
  6. var list = ['facebook', 'twitter', 'instagram', 'linkedin'];
  7. console.log(didYouMean(input, list));
  8. > 'instagram'
  9. // The method matches 'insargrm' to 'instagram'.
  10. input = 'google plus';
  11. console.log(didYouMean(input, list));
  12. > null

这个插件通常用来获取用户可能输入的命令,并做进一步的提示:
比如用户输入 ‘create’,而实际命令是 ‘creat’,就可以利用这个插件给出 ‘did you mean creat?’之类的提示。

commander

命令解析插件,解析用户输入。

commander 中文文档https://github.com/tj/commander.js/blob/master/Readme_zh-CN.md

用法:https://segmentfault.com/a/1190000019350684

  1. program
  2. .command('inspect [paths...]')
  3. .description('inspect the webpack config in a project with vue-cli-service')
  4. .option('--mode <mode>')
  5. .option('--rule <ruleName>', 'inspect a specific module rule')
  6. .option('--plugin <pluginName>', 'inspect a specific plugin')
  7. .option('--rules', 'list all module rule names')
  8. .option('--plugins', 'list all plugin names')
  9. .option('-v --verbose', 'Show full function definitions in output')
  10. .action((paths, cmd) => {
  11. require('../lib/inspect')(paths, cleanArgs(cmd))
  12. });
  13. program.parse(process.argv);

inquirer

命令行交互工具。

  1. const { action } = await inquirer.prompt([
  2. {
  3. name: "action",
  4. type: "list",
  5. message: "文件已存在,请选择操作",
  6. choices: [
  7. { name: "覆盖", value: "overwrite" },
  8. { name: "合并", value: "merge" },
  9. { name: "取消", value: false },
  10. ],
  11. },
  12. ]);
  13. if (!action) {
  14. process.exit(0);
  15. } else if (action === "overwrite") {
  16. await fs.remove(appPath);
  17. }

execa

是更好的子进程管理工具(A better child_process)。本质上就是衍生一个 shell,传入的 command 字符串在该 shell 中直接处理。
例如对安装依赖过程的封装处理:

  1. function executeCommand(command, args, targetDir) {
  2. return new Promise((resolve, reject) => {
  3. console.log(command, args);
  4. const child = execa(command, args, {
  5. cwd: targetDir,
  6. stdio: ["inherit", "pipe", "pipe"],
  7. });
  8. child.stdout.on("data", (buffer) => {
  9. let str = buffer.toString().trim();
  10. // console.log(str);
  11. process.stdout.write(str);
  12. });
  13. child.on("close", (code) => {
  14. logInfo("依赖安装结果:" + code);
  15. if (code !== 0) {
  16. reject(`command failed: ${command} ${args.join(" ")}`);
  17. return;
  18. }
  19. resolve();
  20. });
  21. });
  22. }

download-github-repo / download-git-repo

可以用于下载整个仓库、或者制定分支、指定文件夹、指定文件。

  1. download(repo, destination, callback)

Semver

语义化版本号

globby

使用通配符去搜索文件

cli-service-global

cli-service-global 是对 cli-service 的外层封装,封装了对单个js/vue文件的 service 模式。
基本原理是通过内置的 template 模板文件夹,将要调试的文件注入到 template 中。
template 文件结构如下:

  1. --template
  2. -- index.html
  3. -- main.js

记:构建思路

执行流程

核心代码:

核心流程:

  1. const creatService = function (context, entry, asLib) {
  2. logInfo('注入bable和eslint 插件,以及其他基础配置,通过 globalConfigPlugin 模拟一套完整的配置环境')
  3. return new Service(
  4. context,
  5. {
  6. projectOptions: {
  7. compiler: true,
  8. lintOnSave: true
  9. },
  10. plugins: [
  11. globalConfigPlugin(context, entry, asLib)
  12. ]
  13. }
  14. )
  15. }
  16. exports.serve = function (_entry, args) {
  17. logStep('构建入口信息')
  18. const { context, entry } = resolveEntry(_entry);
  19. logStep('创建service,并执行 serve')
  20. creatService(context, entry).run('serve', args)
  21. }
  22. exports.build = function (_entry, args) {
  23. logStep('构建入口信息')
  24. const { context, entry } = resolveEntry(_entry);
  25. logStep('asLib,是否有目标输出的html文件')
  26. const asLib = args.target && args.target !== 'app'
  27. if (asLib) {
  28. args.entry = entry
  29. }
  30. logStep('创建service,并执行 build')
  31. creatService(context, entry).run('build', args)
  32. }

总结:

  1. 构建入口信息
  2. 判断入口文件是否存在,如果没有传入入口文件则读默认的。
  3. 创建service,并执行 serve
  4. 注入bableeslint 插件,以及其他基础配置,通过 globalConfigPlugin 模拟一套完整的配置环境
  5. 生成全局性的基础配置
  6. 此处配置的模式是 plugin 的模式 {id:"",apply(api,options)=>{}}
  7. 预计此处配置会在 creatAPI GeneratorAPI 部分调用,用于注入 webpack 配置等
  8. 服务启动的入口
  9. serve
  10. { open: true }

cli-service

cli-service 主要包括两个部分:命令部分和服务插件部分,cli-service 将 build、serve、inspct 等命令通过命令插件机制进行管理,提供了一个可扩展命令的服务管理,同时通过 服务插件可以轻易的扩展cli 相关配置和构建依赖、构建内容。

记:构建思路

执行流程

核心代码

读取依赖

从读取依赖的过程,可以看出来通过约定插件名字以 vue 开头的插件作为 cli-service的插件(约定大于配置)。
这个过程包括了命令和构建插件的扩展方式:读取用户输入的插件+pkg中的插件。

  1. this.plugins = this.reslovePlugins(plugins);
  2. reslovePlugins(inlinePlugins, useBuilltIn) {
  3. logStep(
  4. "解析拼接内部插件,以及是否使用 useBuiltIn 等其他插件,这里是 vue-cli-service 插件系统的核心入口"
  5. );
  6. logInfo("这里也说明了 service 内部也采用了 plugin 的模式来实现功能的堆叠");
  7. logInfo("例如:./commands/serve ./command/build ./commands/inspect");
  8. const idToPlugin = (id) => ({
  9. id: id.replace(/^.\//, "built-in:"),
  10. apply: require(id),
  11. });
  12. const buildInPlugins = [
  13. "./commands/serve",
  14. "./commands/build",
  15. // "./commands/inspect",
  16. // "./commands/help",
  17. './config/base',
  18. './config/css',
  19. './config/dev',
  20. // './config/prod',
  21. './config/app'
  22. ].map(idToPlugin);
  23. if (inlinePlugins) {
  24. logStep(`加载其他默认的插件 ${inlinePlugins}`);
  25. return buildInPlugins.concat(inlinePlugins);
  26. } else {
  27. logStep(
  28. `加载 package 中配置的插件:${this.pkg.devDependencies
  29. },${JSON.stringify(this.pkg.dependencies)}`
  30. );
  31. logInfo(
  32. "这里就是为什么cli-service 可以加载pkg 中的插件,而无需额外配置的原因。"
  33. );
  34. logInfo(
  35. "是否是cli-service的插件依靠命名来确定:/^(@vue/|vue-|@[w-]+/vue-)cli-plugin-/"
  36. );
  37. const projectPlugins = Object.keys(this.pkg.devDependencies || {})
  38. .concat(Object.keys(this.pkg.dependencies || {}))
  39. .filter((id) => {
  40. return /^(@vue\/|vue-|@[\w-]+\/vue-)cli-plugin-/.test(id);
  41. })
  42. .map((id) => {
  43. let apply = () => { };
  44. try {
  45. apply = require(id);
  46. } catch (error) { }
  47. return { id, apply };
  48. });
  49. logInfo(projectPlugins);
  50. return buildInPlugins.concat(projectPlugins);
  51. }
  52. }

serve 过程

serve 过程的实现核心即利用 api.chainWebpack 去拼接 webpack 的配置。
在 cli-service 中 webpack 的配置被分为了 四个模块: app、base、css、dev。这个配置模式和cli2 以及各种基于 webpack 的构建工具类似,对大量配置进行分组处理。方便根据需求进行重组。

总结:

这里只记录cli-service 从输入命令开始的主题流程,但是其细节其实是在 serve 和 build 这些详细的命令中的。这些命令是最终执行 build 和 service 的总组装过程, cli-service 是对前期配置的管理,并不负责组装和使用。

即 cli-service 管理cmds、管理 plugins 解析配置资源、管理配置资源。
plugins 产生配置资源。
cmds 使用配置资源。

cli-service 还提供了基础的 webpack 项目构建配置。

记:webpack 插件

  • /webpack/MovePlugin
  • webpack/lib/NamedChunksPlugin
  • html-webpack-plugin
  • copy-webpack-plugin
  • url-loader
  • file-loader
  • pug-plain-loader

总结

cli 生态的全部插件如下:
image.png
不在对其他插件做整理,大概过一遍,了解扩展能力。
通过对cli 三个核心部分的分析,大概了解了 cli 的架构思路,了解了一些实用的方法,库。也了解实现一个架构的基本流程。