这个月我干了一件我们架构组早就想干、但一直被业务排期压着的事:把内部维护了三年的公共组件库从“发布前必须跑一轮构建”改成了“源码即产物”,也就是整套发布链路彻底变成了纯 ESM、零构建。没动任何组件逻辑,只是把发布管线重写了一遍,结果下游十几个项目里,有八个直接删掉了针对组件库的 webpack alias 和 babel 配置,构建时间平均降了 30% 以上。今天把整个过程和踩过的坑完整写出来,给同样在维护公共组件库、被构建链折磨的朋友一个可落地的参考。
这篇文章适合两类人:一类是组件库维护者,想摆脱“每次改一行代码就要等 rollup 跑几十秒”的困境;另一类是业务项目里被公共组件库的叠加构建坑过的人,比如反复出现双实例、样式被 tree-shaking 干掉、sourcemap 对不上这类问题。我会先讲清楚为什么纯 ESM 能解决这些问题,然后按迁移前的体检、改造链路、接入姿势、踩坑记录、不适配场景这个顺序展开,全程都有配置和代码示例,可以直接抄。
1. 为什么非要做纯 ESM:从一次“构建地狱”说起
先还原一下改造前的状态。我们的公共组件库 my-ui 大概 40 多个组件,源码用 TS 编写,内部依赖了自己的两个基础包 my-utils 和 my-icons。传统发布流程是:每个包各自用 rollup 打包成 dist/esm 和 dist/umd 两个目录,再发布到私有 npm。这听起来很常规,但实际用起来有四个问题,我相信维护过组件库的人都能共鸣:
第一,叠加构建。业务项目用 webpack 或 vite 做应用构建,组件库本身又经过一次 rollup 预构建。组件库里依赖的 my-utils 又被 my-icons 依赖,于是一条 import 链路上可能叠加了四到五层构建。改一行底层工具的代码,要等 my-utils 构建、my-icons 构建、my-ui 构建,再等业务项目构建,一次全量验证跑下来要十几分钟。
第二,双实例和版本错位。因为每个包发布时把依赖的版本写死在了 dependencies 里,业务项目经常出现两个版本的 my-utils,React 或者 Vue 被重复打包。这类问题排查起来极其痛苦,instanceof 判断失败、事件总线收不到消息、context 失效,全是这类坑。
第三,sourcemap 对不上。组件库发布的是 rollup 处理后的产物,业务项目 debug 时经过多层 sourcemap 映射,最终看到的代码和源码经常对不上,尤其当业务项目还开了 babel 时,断点位置直接漂移到天边。
第四,构建脚本本身就是维护负担。外部依赖升级导致 rollup 插件兼容性问题、babel 配置要同步、externals 列表要跟着依赖关系改。构建本身成为了一等公民,反而组件本身的开发时间被压缩了。
纯 ESM 方案直接把这些问题从根上砍掉:组件库发布的产物就是源码本身,不做任何转译和打包。浏览器的 import 语法、Node.js 的 ESM loader、vite 的预构建机制,都能直接消费源码。下游项目拿到的是我们写的原始代码,没有中间产物,就没有 sourcemap 失真、双实例、叠加构建这些问题。
| 维度 | CommonJS | ES Module | UMD |
|---|---|---|---|
| 浏览器原生支持 | 不直接支持 | 支持 | 支持 |
| Node.js 原生支持 | 支持 | 支持(Node 12.17+) | 通过 CJS 包装 |
| 静态分析/tree-shaking | 很难 | 原生支持 | 基本不支持 |
| 同步加载 | 支持 | 支持 | 支持 |
| 顶层 await | 不支持 | 支持 | 不支持 |
| 循环依赖处理 | 运行时 | 编译期 | 运行时 |
| 依赖版本复用 | 容易重复 | 天然共享 | 容易重复 |
注意最后一行,ESM 天然共享依赖这一点,是解决组件库双实例问题的关键。只要组件库把外部依赖声明为 peerDependencies,业务项目安装的 React/Vue 版本就是唯一实例,不会因为组件库内部又锁了一份依赖而出问题。
需要模型API调用? 免费领10W Token,多模型网关一键接入 Claude、DeepSeek 等主流模型。
2. 迁移前的体检:先搞清楚组件库的家底
不要一上来就改代码,先做一次系统性的体检。我建议按下面四个维度挨个排查,任何一个不过关,都不能直接切纯 ESM。
2.1 盘点产物和入口
先看 package.json 里的入口字段。老的组件库通常是:
json复制{
"main": "dist/cjs/index.js",
"module": "dist/esm/index.js",
"typings": "dist/types/index.d.ts"
}
要切换成纯 ESM,main、module、typings 这三个字段就只能当兼容入口保留,核心逻辑要全部挪到 exports 字段里。在动手前,先列出当前的产物目录清单、哪些文件是构建生成的、哪些是手写的,避免后面误删。
2.2 静态扫描 CJS 痕迹
这一步是硬性检查。组件库源码里只要还存在以下代码,拉到原生 ESM 环境就会直接报错:
- `requi
