369 - 《如何开发构建工具》

发布于 2023年11月9日

准备明天周会分享,时间 30m 左右,但想了太多,感觉内容够一本书了。时间有限,先写了一部分,应该能凑够 30m 了。后面的内容找时间慢慢补。

大纲

  • 手写一个 Toy Version
    • Toy Version
    • 构建器的基本流程
  • 构建器基础
    • 写 Rust 就是写数据结构
      • module、module graph、chunk、chunk graph
    • JavaScript、TypeScript、JSX
    • CSS、CSS Modules、Auto Prefixer、Inline or Extract
    • 语法错误和兼容
    • AST 的一生
    • Resolve
    • Alias、Externals、Remote Externals
    • Runtime
    • Platform(Browser、Node、Worker 等)
    • Source Map
    • JSON
    • Assets 和 Base64
    • Minify
    • Rust 语言的错误处理
    • Binding
  • 构建器进阶
    • 产物格式(ESM、CJS、UMD、IIFE 等)
    • Worker
    • WASM
    • Polyfill
    • Top Level Await
    • SVG
    • Hash
    • Dead Code Detect
    • Circular Dependency Check
    • Optimize Package Imports
    • 多 Entry
    • 产物性能
    • 基于 Rust 的插件体系
    • Bundless
  • 构建器高级
    • Hot Module Replacement、Watch Mode
    • Tree Shaking
    • Code Splitting
    • Scope Hoisting
    • Constant folding
    • 基于 Rust 的构建性能
    • 缓存
    • 面向用户的插件设计
  • 构建器的工程化
    • 日志
    • 火焰图、puffin
    • Benchmark
    • Less
    • 发包
  • 生产级的构建器
    • 用 E2E 测试确保稳定性
    • CodeMod
    • Edge Case 收集
    • Tree Shaking 的稳定性
    • 监控
  • 备选
    • Case Cansitive Detect
    • Stats
    • Manifest

手写一个 Toy Version

Toy Version

注:这部分从 284 - 《手写 Toy Bundler》 复制而来。

早在写 Mako 之前,先写了一个构建器的 Toy Version。代码见 https://github.com/sorrycc/toy-bundler

基本思路是这样,

1、resolve config,解析用户配置,包括确定入口文件
2、build,生成模块依赖图谱
3、generate,根据依赖图谱生成代码

build 是以入口文件为起点构建依赖图谱。基本思路如下,从入口文件开始,做 load、parse、transform、analyze_deps 和 resolve,然后把依赖添加到队列里接着跑,直到分析完所有文件为止。

const seen = new Set();
const queue = [entryPoint];
while (queue.length) {
  const module = queue.shift()!;
  if (seen.has(module)) continue;
  seen.add(module);

  // 1、load
  let content = load();
  // 2、parse
  let ast = parse(content);
  // 3、transform
  let { content } = transform(ast);
  // 4、analyze
  let { deps, exports, imports, ... } = analyze(ast, content);
  // 5、resolve
  let resolvedDepsMap = resolve(deps, module);

  let metaData = { id, content, ast, deps, resolvedDepsMap, imports, exports, ... };
  modules.set(module, metaData);
  queue.push(...resolvedDepsMap.values());
}

generate 是基于 build 生成的依赖图谱生成最终代码,包含 runtime 的处理、module 转 code 以及将其封成可以在浏览器里跑的版本,以及 tree-shaking、code splitting 等也是在这边处理。

module 渲染逻辑如下,

import foo from './foo';
foo();

↓ ↓ ↓

const foo = require(/* ./foo module id */123);
foo();

↓ ↓ ↓

define(/* current module id */0, (module, exports, require) => {
const foo = require(/* ./foo module id */123);
foo();
});

最后通过一个 runtime 把所有内容拼起来,针对 node、browser 会有不同的 runtime。

const modules = new Map();
const define = (name, moduleFactory) => {
  modules.set(name, moduleFactory);
};
const requireModule = (name) => {
  const moduleFactory = modules.get(name);
  const module = {
    exports: {},
  };
  moduleFactory(module, module.exports, requireModule);
  return module.exports;
};
requireModule(/* entry module id */0);

构建器的基本流程

没画 Mako 的图,先以 esbuild 的架构图用于讲解下 Mako 的流程,大差不差。

分两个阶段,1)Build,2)Generate。

Build 阶段的目的是为了构建一个完整的 Module Graph。所以会先收集 Entry,然后以 Entry 为入口找到所有模块,再对每个模块并行做 Load、Parse、Transform、Analyze Deps、Resolve、Add to Module Graph 的过程。

Generate 阶段是真正复杂的部分,很多复杂的功能都是在这个阶段完成。在 Mako 的流程里,会先做 Tree Shaking,再做 Chunk 的 Group 和 Optimize 以生成 Chunk Group,再做一次 transform,再针对每个 Chunk 里的每个模块的 Runtime 代码注入、Ast to Code、压缩、Source Map 生成等。

Transform 简单来说就是针对 AST 的修改。我们会发现,Build 和 Generate 里都有 Transform。Build 里的 Transform 里会做 React 相关、Env Replacer 环境变量替换、Try Resolve 允许 try 语句里 require 模块失败、Define 替换及常量折叠、import() 到 require 的转换以禁用拆包、基于 targets 浏览器等平台版本的 JS 语法转换等;Generate 里的 Transform 会做 ESM 到 CommonJS 的转换,模块里 import 语句 source 依赖的替换等。为啥要拆成两部分做?因为有些事情不能做太早,比如在 T

内容预览已结束

此内容需要会员权限。请先登录以查看完整内容。