Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

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

Open
bosens-China opened this issue Jun 20, 2024 · 0 comments
Open

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

bosens-China opened this issue Jun 20, 2024 · 0 comments
Labels
Node系列 和node.js相关内容

Comments

@bosens-China
Copy link
Owner

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 中提倡使用异步语法,但是有一些情况使用同步语法更合适。

最后

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

@bosens-China bosens-China added the Node系列 和node.js相关内容 label Jun 20, 2024
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Node系列 和node.js相关内容
Projects
None yet
Development

No branches or pull requests

1 participant