yliu

时来天地皆同力,运去英雄不自由

从 Vue 构建模块你可以学到什么?


spacexcode-coverview-2527@2x

最近在给公司内部低代码平台进行升级,原本是单一仓库,里面有各种模块,例如:

  • utils
  • view-render
  • form-render
  • fetch
  • form-design
  • ...

它们最终给其他产品线使用的时候是通过 Vue Cli 打包成一个 umd 的 js 文件,但是这样会带来一系列问题,所以就准备调整成 monopore 的形式,讲上面的模块拆分成一个个的单体仓库,以及后续版本发布也会基于 npm 的形式来管理。

但是首先摆在眼前的就是对 packages 下目录的包要如何进行构建分发,这里调研了下最终参考 Vue 的构建过程。

这篇文章不介绍 Vue 是如何构建整个流程的,只是介绍在构建中可以收获什么知识点。

node: 前缀

image

打开 scripts/build.js 文件,映入眼帘的就是 node: 前缀名称。

你可能很好奇,之前使用 node 包的时候都是直接引入呀,为什么现在还出现了这种语法。

其实这个是 node18 引入的特性,简单来说就是 node18 引入了一些新模块,例如 test,但是 test 这个 npm 上已经有了,这样的话就会有歧义,到底你要使用 node 的包还是 npm 别人发布的呢?

所以就添加了 node: 的前缀

import test from "node:test";
import assert from "node:assert";

test("synchronous passing test", (t) => {
  // This test passes because it does not throw an exception.
  assert.strictEqual(1, 1);
});

其次,因为 node: 是官方的前缀,也可以一定程度避免安全性问题,例如可能会有一个包叫做 expres 它跟 express 非常接近,可能会存在误导性。

还有也会带来性能上的提升,有了 node: 前缀可以避免查找 node_modules 目录这一过程,直接加载内置模块。

补充阅读

控制并发数量

在 build 过程中,如果一个个顺序调用会导致构建时间太长了,就需要有一个机制来管理并发数量

/**
 * Runs iterator function in parallel.
 * @template T - The type of items in the data source
 * @param {number} maxConcurrency - The maximum concurrency.
 * @param {Array<T>} source - The data source
 * @param {(item: T) => Promise<void>} iteratorFn - The iteratorFn
 * @returns {Promise<void[]>} - A Promise array containing all iteration results.
 */
async function runParallel(maxConcurrency, source, iteratorFn) {
  /**@type {Promise<void>[]} */
  const ret = [];
  /**@type {Promise<void>[]} */
  const executing = [];
  for (const item of source) {
    const p = Promise.resolve().then(() => iteratorFn(item));
    ret.push(p);

    if (maxConcurrency <= source.length) {
      const e = p.then(() => {
        executing.splice(executing.indexOf(e), 1);
      });
      executing.push(e);
      if (executing.length >= maxConcurrency) {
        await Promise.race(executing);
      }
    }
  }
  return Promise.all(ret);
}

这个函数会在 buildAll 调用

async function buildAll(targets) {
  await runParallel(cpus().length, targets, build);
}

targets 简单来说就是 packages 目录下的一系列包名 targets: string[]

这个函数会通过 await 特性来调整并发数量,简单概括一下流程

  1. 执行 for of 循环
  2. 判断 source 是否大于 maxConcurrency
  3. 如果大于则进入 if 循环中

这里补充一下 if 是如何工作的

if (maxConcurrency <= source.length) {
  // 微任务队列,会在本次宏任务结束后清空
  const e = p.then(() => {
    executing.splice(executing.indexOf(e), 1);
  });
  executing.push(e);
  // 如果 executing 数量大于了 maxConcurrency 等待 executing 执行
  // Promise.race 作用是只要有一个任务变化,就返回最先变动的promise,这里利用了这个机制,任务变化就让它往下面走
  if (executing.length >= maxConcurrency) {
    await Promise.race(executing);
  }
}
  1. Promise.all 执行完成全部任务

整体流程就是这样,可以理解 all 会一次性并发掉所有 maxConcurrency 数量的任务,如果 source 任务数量大于 maxConcurrency 则会在 if 语句中剔除掉

祖师爷就是祖师爷,写的很短小精剪

execa

这里并没有直接调用 rollup 的 api 来完成构建,我觉得这点也可以说下

buildAll 这个函数用到了 cpus().length ,基于 cpu 的核心数量来构建任务。

每次 execa 都会创建一个子进程,可以最大程度利用机器性能,虽然没有通过多进程的形式来构建,但是多进程的创建也是一笔花销。

await execa(
  "rollup",
  [
    "-c",
    "--environment",
    [
      `COMMIT:${commit}`,
      `NODE_ENV:${env}`,
      `TARGET:${target}`,
      formats ? `FORMATS:${formats}` : ``,
      prodOnly ? `PROD_ONLY:true` : ``,
      sourceMap ? `SOURCE_MAP:true` : ``,
    ]
      .filter(Boolean)
      .join(","),
  ],
  { stdio: "inherit" }
);

其次 stdio: "inherit" 的意思是进程将共享当前进程的标准输入、输出和错误流。也就是说,子进程的输出会直接显示在当前进程的控制台上,输入也会直接来自当前进程的控制台。

这样我们就会看到 rollup 的打包的信息生成在控制台中。

require

require 是 cjs 的导入模块标准,它不同于 esm 会同步加载模块。但是有的时候也会想使用 require,例如加载一个 json 文件,如果不通过 require 就需要通过 import() 的语法了,import() 是一个异步 api,所以就会带来一些问题。

再来看 Vue 是怎么处理的。

import { createRequire } from "node:module";
const require = createRequire(import.meta.url);

通过 createRequire 创建了 require,这样就可以在 esm 模块中导入 cjs 的模块了。

const pkg = require(`${pkgDir}/package.json`);

例如在这里就直接读取了对应 packages 下的 package.json 文件了

rm

node 默认的 fs.rm 只会删除一层目录,如果目录里面嵌套目录就会导致删除不了。

因此会有一系列包,但是 node 现在支持删除嵌套目录了,使用方法也很简单,通过 recursive 选项。

if (!formats && existsSync(`${pkgDir}/dist`)) {
  await fs.rm(`${pkgDir}/dist`, { recursive: true });
}

这里也可以聊一下 existsSync 这个 api,它会判断对应的文件或者目录是否存在,是一个同步语法,虽然在 node 中提倡使用异步语法,但是有一些情况使用同步语法更合适。

最后

如果内容有错误地方欢迎指出。