前端库为什么要同时产出 ESM、CJS 和类型声明?
简化版
前端库同时产出 ESM、CJS 和 .d.ts 是为了兼容不同消费环境:现代构建工具更适合 ESM,Node 或旧工具可能需要 CJS,TypeScript 用户需要类型声明。关键是正确配置 exports、main、module、types,避免双包陷阱和路径不一致。
详细版
常见产物:
- ESM:支持 Tree Shaking,适合 Vite、Webpack、Rollup 等现代工具。
- CJS:兼容 CommonJS 环境。
.d.ts:给 TypeScript 提供类型。- CSS 或样式产物:组件库常见。
配置重点:
package.json中声明入口。- 使用
exports控制子路径导出。 - 类型声明路径要和运行时代码对应。
- 避免同一包被 ESM/CJS 各加载一份。
- 测试真实消费项目,而不只测源码。
完整版教学
一、库和应用的构建目标不同
业务应用构建后通常直接部署给浏览器,目标是生成可运行页面。库的目标是被别人安装和构建,所以要考虑消费方环境:有人用 Vite,有人用 Webpack,有人跑 Node,有人用 TypeScript。库产物要成为稳定契约。
这就是为什么组件库构建比应用构建更讲究入口、模块格式、类型声明和副作用标记。库发出去以后,消费方怎么打包不是你完全能控制的。
源码 TypeScript
├─ dist/index.mjs 给 ESM 消费
├─ dist/index.cjs 给 CJS 消费
└─ dist/index.d.ts 给 TS 类型系统
二、ESM 更适合现代前端构建
ESM 使用静态 import/export,构建工具能更容易分析依赖关系,做 Tree Shaking 和代码分割。现代浏览器和工具链都越来越偏向 ESM。对前端组件库来说,ESM 通常是主力产物。
export { Button } from './button'
export { Modal } from './modal'
如果库只产 CJS,某些 Tree Shaking 会变差。比如用户只引入 Button,构建工具却难以安全删除 Modal 相关代码,最终包体积变大。
三、CJS 仍然为了兼容存在
CommonJS 使用 require/module.exports,在 Node 生态和部分旧工具中仍有需求。虽然前端趋势是 ESM,但一些测试工具、脚本环境或老项目仍可能消费 CJS。库是否产 CJS,要看目标用户。
| 格式 | 优点 | 适合 |
|---|---|---|
| ESM | 静态分析、Tree Shaking 好 | 现代前端构建 |
| CJS | 老生态兼容好 | Node 脚本、旧工具 |
| UMD/IIFE | 直接 script 引入 | CDN 老场景 |
.d.ts | 类型提示 | TypeScript 用户 |
不是所有库都必须三格式齐全。如果明确只支持现代 ESM,也可以简化,但要在文档和 package 配置里说清楚。
四、exports 是现代入口控制中心
过去常见 main/module/types,现在更推荐用 exports 精确声明入口和子路径。它能限制用户访问未公开内部文件,也能为 import/require/types 提供不同入口。
{
"main": "./dist/index.cjs",
"module": "./dist/index.mjs",
"types": "./dist/index.d.ts",
"exports": {
".": {
"import": "./dist/index.mjs",
"require": "./dist/index.cjs",
"types": "./dist/index.d.ts"
}
}
}
配置错了会出现“运行能跑但类型找不到”或“类型指向旧文件”的问题。
五、双包陷阱要小心
如果同一个库在应用里一部分通过 ESM 加载,一部分通过 CJS 加载,可能出现两份实例。对纯函数库影响小,对有全局状态、上下文、单例的库影响很大。比如状态管理库被加载两份,会出现 Provider 和 Consumer 不在同一个实例上的问题。
数字例子:组件库本身 80KB,如果 ESM 和 CJS 被各打进一份,就变成 160KB;更糟糕的是单例状态不一致,问题很隐蔽。库作者要通过 exports、文档和测试减少这种风险。
六、要测试“发布后的包”
只测源码不够。库发布前应打包,再用示例项目或 fixture 以真实安装方式消费:测试 ESM import、CJS require、类型提示、子路径导入、样式引入和 Tree Shaking。很多问题只有在 dist 包里才暴露。
记忆钩子:应用构建给浏览器用,库构建给别人的工具链用;入口和类型就是库的合同。
七、常见误区与追问
- 误区:源码能跑,库发布就没问题。 消费方用的是 dist、exports 和类型声明,必须测试发布产物。
- 误区:有 main 字段就够了。 现代包更需要 exports、types 和 import/require 条件入口。
- 误区:ESM 和 CJS 同时产出一定没有风险。 配置不当可能导致双包陷阱和重复实例。
- 追问:为什么 ESM 更利于 Tree Shaking? 静态导入导出让构建工具更容易分析未使用代码。
- 追问:类型声明如何生成? TypeScript 可通过 declaration 输出
.d.ts,也可用 API Extractor 等工具整理。 - 追问:组件库 CSS 怎么处理? 可单独产出 CSS、按组件拆分样式,并通过 sideEffects 标记避免误删。
八、加强记忆
库产物用“ESM 给现代构建,CJS 给兼容,d.ts 给类型,exports 管入口”来记。库不是自己跑完就行,而是要让各种消费项目稳定使用。发布前测试真实 dist 包,特别关注子路径、类型、样式和双包陷阱。