跳到主要内容

04 · 构建管线:build-all.mts 怎么打包 widget

这章讲工程侧:从 src/<name>/index.tsx 到 server 能 serve 的 assets/<name>.html,中间发生了什么。

1. 要解决的小问题

每个 widget 是一棵 React 树,依赖 React、Tailwind、可能还有 mapbox/three。但 server 的 read_resource 只想吐一个 HTML 字符串,host 在沙箱里加载它就能跑。所以构建目标是:每个组件 → 一个自包含、可远程加载的 HTML 壳

2. 顶层流程

build-all.mts 不是一次 Vite build,而是给每个 target 跑一次独立 build,最后统一加哈希、生成 HTML。

对 targets 里每个 name(如 "pizzaz"、"shopping-cart"):
① 找到 src/<name>/index.tsx
② 收集该目录下的 CSS + 全局 src/index.css
③ Vite build:用「虚拟入口」把 CSS import 进来,单文件打包
→ 产出 assets/<name>.js + <name>.css

全部 build 完:
④ 用 package.json version 算 4 位哈希 h
⑤ 把 <name>.js → <name>-<h>.js(CSS 同理)
⑥ 为每个 name 生成两份 HTML:<name>-<h>.html 和 <name>.html

target 清单写死在 build-all.mts:20-44targets 数组(也支持 --target xxx 只打一个)。

3. 巧妙处:虚拟入口包注 CSS

Vite 默认不会把任意 CSS 塞进一个组件入口。脚本用一个自写插件 wrapEntryPlugin(build-all.mts:54-84)造了个虚拟模块当真正入口,模块内容动态拼出来:

// 示意,非源码 —— 虚拟入口模块的内容
import "src/index.css"; // 全局 + 该组件的 CSS,先注入
export * from "src/pizzaz/index.tsx";
import * as __entry from "src/pizzaz/index.tsx";
export default (__entry.default ?? __entry.App); // 兼容 default 或 App 导出

这样 CSS 被打进同一个 bundle,产物里不会漏样式。resolveId/load 钩子识别这个虚拟 ID(build-all.mts:61-82)。

4. 单文件产物的关键配置

build 配置(build-all.mts:135-156)几个要点:

配置为什么
inlineDynamicImportstrue把动态 import 也内联,保证只产一个 JS
cssCodeSplitfalseCSS 不拆分,合成一个 <name>.css
entryFileNames${name}.js文件名可预测(server 按名字找)
emptyOutDirfalse多次 build 不互相清空(开头已手动 rmSync,:86)

5. 版本哈希与 HTML 壳生成

哈希取自 package.jsonversion(build-all.mts:172-176),sha256 取前 4 位。所有产物重命名加上 -<h>(:179-187)。

最后为每个组件写出 HTML(build-all.mts:222-237)。这个壳极简——它不内联 JS,而是用 <script src> 指向 BASE_URL 上的远程 JS/CSS:

<!-- 示意,非源码 —— 生成的 HTML 壳 -->
<head>
<script>window.__APP_URL_CONFIG__ = {"apiBaseUrl":"...","assetsBaseUrl":"..."};</script>
<script type="module" src="http://localhost:4444/pizzaz-<h>.js"></script>
<link rel="stylesheet" href="http://localhost:4444/pizzaz-<h>.css">
</head>
<body><div id="pizzaz-root"></div></body>

注意两点:

  • 根节点 id = <name>-root,正好对上 widget 入口里 getElementById("<name>-root")(回扣 02 章 §5)。
  • BASE_URL 可配(build-all.mts:192-200):本地默认 http://localhost:4444,部署时设环境变量 BASE_URL 指向你的 CDN。__APP_URL_CONFIG__ 还带个 apiBaseUrl,给 widget 拼后端 API 用(如 Cards Against AI 的事件流)。

6. 这与 server 怎么接上

server 启动时读 assets/<name>.html(01 章 §7)。所以链路是:build-all.mts 写出 HTML → server 读进内存 → read_resource 吐给 host → host 在 iframe 里加载这个壳 → 壳的 <script src>BASE_URL(pnpm run serve 的 4444)拉真正的 JS → React 挂载到 #<name>-root

7. 代码地图

主题文件路径符号名
target 清单build-all.mtstargetscliTarget
虚拟入口注 CSSbuild-all.mtswrapEntryPlugin
单文件打包配置build-all.mtscreateConfig(inlineDynamicImports)
哈希与 HTML 生成build-all.mts(172 行起的 hash / HTML 循环)
根节点约定src/kitchen-sink-lite/index.tsxgetElementById("kitchen-sink-lite-root")