MAW / ESM 迁移图解一步迁移图解 →← 回到迁移工作台
HOW WE GOT HERE

这套系统是怎么来的:
2.3 万行单文件 → 192 个模块

这批代码从来没有被「重写」过——只是被反复地切割和包装,构建系统再把碎片按原始顺序拼回去。一张图看三个月:

2026-07 · 出生 editor.js 7,288 行 editor-utils waveform.js i18n / guide 没有模块系统:script 标签排队,共享全局作用域 AST 切割 #136–156 2026-09-25 · 大切割 boot state cues io styles shared utils …192 个文件(示意),语句逐字保留 editor-scripts.txt 顺序清单(= 原段落顺序) 目录按职责分:boot / state / cues / io / markers / media / styles / ui + shared/ esbuild #181 2026-10 · 打包 + 出 Git editor-bundle.js IIFE · 压缩 · 哈希指纹 Server 页面 便携 HTML 产物不进 Git · 启动时按哈希自动重建

切割只动「文件的边界」,不动「执行的顺序」——清单顺序就是当年大文件里的段落顺序,所以全局行为一天都没变过。这就是全部 trick。

01时间线细账

2026-07-26

出生:一切都是 classic

项目带着 web/editor.js 和几个卫星脚本问世。没有模块系统,靠 script 加载顺序保证「用之前先定义」;edit.py 把脚本按顺序内联进单文件 HTML,让没有 Node 的机器也能跑。

2026-08-13 · cbf5683c · Phase 0-1

不拆文件,先立规矩

企划案定调:不换框架、不引打包器、不改任何行为。埋下两块基石:editor-scripts.txt 有序清单 + window.MAWE 注册表。企划案写完后的一个多月功能照常往大文件堆,开拆时已经 23,273 行。

2026-09-25 → 09-28 · #136 / #154 / #155 / #156

机械大切割:editor.js 消失

AST 工具按「语句」为单位切割(重复声明检查、隐式全局扫描一整套),方案写成 JSON 规格再机械重放。语句逐字保留,按职责分目录。切完 editor.js 不复存在。

2026-10-08 · #181

esbuild 构建系统落地

「工厂形状」的文件从自己调 MAWE.register 机械改写成 export function,登记动作由构建器统一生成(§02 的 factoryN)。

2026-10-11 · 54de1a15

bundle 出 Git

压缩产物不进仓库:启动时校验哈希、过期自动重建。仓库不被压缩代码刷屏,发布包仍零 Node 依赖。

2026-10-16 → 10-18

路线图与第一条消费边

机械迁移实验(红/绿两轮)产出 G00–G17 批次计划;G01 落地第一条生产消费边:editor-utils 转 init 模块,lrc 改用真 import。

192 个清单文件的形态构成 · 2026-10-20 classic 直插 · 128 ESM 工厂 · 63 init 1 工厂只登记不执行 · init 在清单位次被调用 · classic 粘进共享作用域
62文件在用 AsrEditorUtils 门面
304门面条目数
33editor-utils 里的 resolve
69全仓 resolve 等待逐边替换
32 / 25个工厂重名 createUtilsModule / createWaveformModule

02解剖 bundle:构建器生成的虚拟入口

页面加载的从来不是 192 个散文件,而是构建器现场「手写」的一个入口文件,交给 esbuild 打包:

editor-scripts.txt(192 行顺序) editor-modules.json(64 条登记) shared/utils/lrc.jsexport createUtilsModule ⓕ shared/editor-utils.jsexport initEditorUtils ⓕinit editor/**/wiring-*.jsclassic ⓒ editor/state、cues、io …classic ⓒ ×128 构建器现场生成的虚拟入口 editor-entry.mjs import { createUtilsModule as factory25 } from 'maw-factory:shared/utils/lrc.js'; import { initEditorUtils as factory36 } … function initializeLegacy() { MAWE.register('utils-lrc', factory25) factory36(); // 第 49 位调用 classic 源码粘贴 classic 源码粘贴 …×128 段 …按清单顺序 逐字嵌入 window.MAWE 注册表 名字 → 工厂 的 Map(运行时查询处) } initializeLegacy(); esbuild 打包 · 压缩 editor- bundle.js 单 IIFE Server 页面 便携 HTML 校验:classic 文件必须能按普通脚本解析, 谁偷写 import 就构建失败——防未登记缺口

factoryN 是什么?编号 = 模块在登记表里的数组下标(lrc 第 25 → factory25)。必须起别名,因为 32 个工厂都叫 createUtilsModule、25 个都叫 createWaveformModule,同名 import 无法共存。这个名字只活在构建入口里,压缩后连名字都没了。

// 64 个登记模块全部走真 ESM import;classic 粘进同一个函数作用域
import { createUtilsModule as factory25 } from 'maw-factory:shared/utils/lrc.js';
function initializeLegacy() {
  window.MAWE.register('utils-lrc', factory25);   // 工厂:只登记,不执行
  factory36();                                   // init:在清单位次直接调用
  // …128 个 classic 文件的源码逐字粘贴……
}
initializeLegacy();

03黑话地图:六个词,两张图

生产流水线管「模块怎么找到彼此」,装配现场管「什么时刻执行什么」。

图 A · 生产流水线(模块怎么找到彼此) ② 工厂 utils/data.jsexport create… utils/lrc.jsexport create… …63 个工厂函数体 = 原代码 ① 注册表 window.MAWE 'utils-lrc' → factory25 'utils-data' → factory7 … register ⑥ init editor-utils.js 在清单第 49 位被调用 resolve ×33 import(G01 第一条边) ③ 依赖袋 2 30 300 播种:宿主能力 越滚越大:工厂拿到「到目前为止的袋子」, 返回值再并回去 → 顺序是硬约束 ④ 门面 AsrEditorUtils window 上的巨型对象 304 个条目 · 名字从不变 组装 62 个消费者文件:一个都不用改, 门面背后全换,它们毫无感知
图 B · 装配现场(什么时刻执行什么) 页面 DOM 播放按钮 字幕文本框 波形画布 ⑤ 接线 addEventListener(… ) ×39 个文件 initializeLegacy() · classic 共享作用域 boot state 第 49 位 io … wiring … 192 清单走到哪一行就执行哪一段;wiring 文件在这里「监听」DOM,顺序即语义 第 49 位调用 factory36() = initEditorUtils() editor-utils.js(init) 代码在 ESM 世界,动作发生在清单原位 window.MAWE 注册表 resolve 查询在此发生

六张词卡(编号对应上图 ①–⑥):

①注册表MAWE registry

「名字 → 工厂函数」的 Map。register 存、resolve 取,过渡期用它替代「直接叫别的文件里的名字」。

window.MAWE.register('utils-lrc', factory25); window.MAWE.resolve('utils-lrc', helpers)

退役:G04

②工厂factory

被 export function 包了一层的原代码,函数体一字未改;调用它才产出 API。构建器强制「恰好一个导出函数、零求值副作用」。

export function createUtilsModule() { return Object.freeze({ parseLrcSegments }); }

现状 63 个;被真 import 逐个替换后自然转正

③依赖袋helpers

传给工厂的越滚越大的对象,替代旧全局作用域。工厂拿到「到目前为止的袋子」,返回值并回去——顺序因此是硬约束。

Object.assign(helpers, MAWE.resolve('utils-srt', helpers)); const { stripMassMarkers } = dependencies; // srt.js 开头解构

editor-utils 内有 33 次按序 resolve

④门面facade

旧世界留给消费者的总服务台:304 个条目的巨型对象。名字不变、成员位置不变,背后组装方式全换——62 个消费者零修改。

window.AsrEditorUtils = { …304 项… }; MAWE.register('editor-utils', () => window.AsrEditorUtils);

退役:G04

⑤接线wiring

把 DOM 事件连到逻辑上的纯粘合代码(39 个 editor-wiring-*.js),只在加载时执行 addEventListener,自己几乎不定义逻辑。

MaweDom.cuePanelPrev?.addEventListener('click', () => MaweCuePanel.navigateCuePanel(-1));

去向:G16 启动与接线收口

⑥init 模块init module

「在清单位次被执行的真 ESM」。工厂只会登记不会执行,必须在正确时刻运行的组装逻辑就用 init 表达:构建器在原位调用它。

// 构建器在清单第 49 位生成: factory36(); // → initEditorUtils()

现状 1 个(editor-utils.js,G01 刚升级)

04init 的组装全过程:袋子怎么长成门面

01 factory36(); 构建期:清单第 49 位 生成一句调用 02 袋子 ×2 播种:宿主能力 gapRemove · navigator 03 注册表 袋子 ×300 resolve ×33,每调一次 袋子长一层(顺序承重) 04 解构 ~300 个名字到 局部作用域(写短名用) 05 AsrEditorUtils 304 个条目 组装门面:纯函数工具箱 (LRC/SRT 解析、时间线…) 06 MAWE 门面自己 登记回注册表 为什么它是第一个 init?——把枢纽升级成真 ESM 后,「把 resolve 逐条换成 import」就能在模块内部推进(lrc 已是第一条),而门面照旧骗过全部消费者。 G04 之后注册表清空、门面退役,这套组装整体换成静态 import 图。

05为什么会长出一个 build 系统

不是野心,是三步各自回应一个具体约束:

STEP 1 · 08-13先有清单,没有打包

清单 192 行 内联 Server 页 便携 HTML edit.py / serve.py 做拼接,用户机器零 Node

产品必须同时支持「单文件便携 HTML」和「localhost 服务」,且用户机器没有 Node——第一版刻意不用打包器。

约束:便携 HTML 零 Node 可跑

STEP 2 · 10-08再上 esbuild

esbuild bundleIIFE·压缩 import 依赖图 + 压缩 + 缓存哈希 手工内联成了主要维护成本

文件数到 192、出现真 import 依赖图。企划案早留了口子:「若无构建装配成为主要维护成本,可单独评估 esbuild」。

约束:与「是否用 React」分开决策

STEP 3 · 10-11bundle 出 Git

Git 仓库 只进源码与清单 bundlesha256 指纹 启动时校验 过期自动重建 仓库不再进压缩产物;贡献者从源码跑才需要 Node

源码仓库不被压缩产物刷屏,发布包自带产物。代价是「改了 web/ 必须重建」纪律与四层哈希新鲜度校验。

约束:源码 / 产物 / 发布链分层

06脚手架的拆除时间表

这些词只在迁移中间态里有意义——路线图的终点就是它们逐个消失:

G01 · 现在 第一条 import 已落地 (lrc → editor-utils) 注册表 袋子 门面 G04 共享聚合退役 · 注册表清空 wiring 接线 G16 启动与接线收口 顺序清单 G17 · 终点 清单退役 · 纯 ESM

迁移完成之后剩下的就是普通的 ESM 模块——现在的复杂度是施工脚手架,不是永久架构。想看其中一根线怎么换,去图解:一步迁移(以 LRC 为例)。

← 迁移工作台一步迁移图解 →

史实与数字来自 git 历史(cbf5683c / #136–156 / 09e08633 / 54de1a15 / 0eb1bab0)与企划案;统计截至 2026-10-20。