要让组件库在 npm 上同时支持 CommonJS 兜底和 ESM 高效 Tree Shaking,关键是用 exports 字段精准控制不同环境下的入口路径,并确保 ESM 路径本身具备可摇树结构:ESM 入口需命名导出、扁平化、无默认聚合;CJS 入口兜底;types 同步映射;sideEffects 显式声明;产物目录按格式严格分离。

要让组件库在 npm 上同时支持 CommonJS 兜底和 ESM 高效 Tree Shaking,关键不是“兼容两种格式”,而是**用 exports 字段精准控制不同环境下的入口路径,并确保 ESM 路径本身具备可摇树结构**。Node.js 从 v12.20+ 开始支持 exports,现代打包工具(Rollup/Vite/Tsup)和 bundler(Webpack 5+/Vite)都能正确解析它。
exports 字段必须分环境声明,不能混用 main/module
main 和 module 是旧式双入口方案,已被 exports 取代。继续保留它们不仅冗余,还可能引发解析冲突(比如某些工具优先读 main 而忽略 exports)。正确写法是只用 exports,明确区分:
-
ESM 环境(推荐、支持 Tree Shaking):通过
"import"条件指向纯 ESM 入口(如dist/index.esm.js或dist/index.mjs) -
CommonJS 环境(兜底、不摇树):通过
"require"条件指向 CJS 入口(如dist/index.cjs.js) -
类型声明需同步映射:用
"types"或"typesVersions"指向统一的.d.ts文件(通常放在dist/index.d.ts),确保 TypeScript 不因格式切换丢失类型
ESM 入口必须是命名导出 + 单文件粒度
Tree Shaking 能生效,靠的是静态分析 import/export 关系。如果 ESM 入口是 export default {...} 或聚合对象导出,摇树会失效。正确结构是:
- 每个功能函数/组件单独一个文件(如
src/button/index.ts),使用export const Button = ... - 入口文件
src/index.ts只做重新导出:export { Button } from './button'; export { Icon } from './icon'; - 打包后 ESM 产物保持扁平化命名导出结构,不打包成默认对象
package.json 中 sideEffects 必须显式声明
即使所有模块都用 ESM,若存在副作用(如样式注入、全局 polyfill、console.log),打包工具会保守保留整个模块。解决方式:
- 设
"sideEffects": false表示整个库无副作用,最激进优化 - 若部分文件有副作用(如
src/styles/index.css),则写成"sideEffects": ["*.css", "*.scss"],仅排除这些文件 - 注意:
sideEffects仅对 ESM 导入路径生效;CJS 兜底路径(require)不参与摇树,所以副作用不影响 CJS 使用者
构建产物目录需严格分离格式,且 exports 指向明确
不要把 ESM 和 CJS 文件混放在同一目录下(如都放 dist/),避免字段误配。建议结构:
-
dist/esm/index.js(ESM 格式,无require,含export) -
dist/cjs/index.js(CJS 格式,含module.exports) -
dist/index.d.ts(统一类型声明)
对应 exports 配置示例:
"exports": {
".": {
"import": "./dist/esm/index.js",
"require": "./dist/cjs/index.js",
"types": "./dist/index.d.ts"
}
}
这样,import { Button } from 'your-lib' 走 ESM 路径并可摇树;const { Button } = require('your-lib') 走 CJS 路径,完全兜底。











