ESM分包

需求来源
我们最近要开发一个组件给业务方使用,网页对于性能有较高的需求。而我们的组件代码量比较大,会导致组件的 CDN 比较大,加载耗时较长。因此我们需要对组件进行分包处理,将原本较大的 CDN 拆分为多个相对较小的 CDN。
之前的项目打包时支持导出 ESM 和 UMD 两种形式。但 UMD 格式是为了兼容不同浏览器模块化规范的处理,其产物没有包和模块的概念,不支持分包。因此我们只能选择 ESM 分包方案。
ESM 与 UMD 的区别
- ESM 是官方标准,提供静态导入/导出、原生异步 import(),是实现代码分割的根本底层
- UMD 是兼容包装,把库封装成一个同步闭包,让它能够在 AMD、CommonJS、全局三种环境中使用
- 关系:UMD 常常是 ESM → 打包 → UMD 的产物;UMD 失去 ESM 的静态特性后,就不再具备代码分割的先决条件
为什么 UMD 天生不支持代码分割
- 设计为一次性同步加载,没有运行时的 Chunk Registry
- 没有静态依赖信息,打包工具只能把它当作黑盒
- 代码分割需要共享闭包/模块缓存,而 UMD 把实现闭在单个 IIFE 中,后续块无法访问同一作用域
方案:CDN 使用 ESM + 代码分割
做法
- 将 CDN 产物改为 ESM 目录输出:
output: { format: 'es', dir: build/aigc/${version}/esm } - 启用 manualChunks 做稳定分包:
vendor:node_modules/**(如lodash,dayjs等)core:src/core/**,src/index.common.ts依赖的基础模块ui:src/ui/**layers: 按图层家族拆包,如src/layer/preset/bar/**,line/**,pie/**…- 可选:
insight/**,hook/**单独包
- 配置
entryFileNames/chunkFileNames/assetFileNames带内容 hash,利于 CDN 缓存 - 运行时改为
<script type="module" src="/aigc/${version}/esm/index.[hash].js"></script>,浏览器会自动按需加载子 chunk
{
"dir": "esmOutputDir",
"format": "es",
"banner": "",
"sourcemap": true,
"entryFileNames": "index-[hash].js",
"chunkFileNames": "chunk-[name]-[hash].js",
"assetFileNames": "assets/[name]-[hash][extname]",
"manualChunks": {
"id": "id.replace(/\\\\/g, '/')",
"if": "normalizedId.includes('/node_modules/')",
"return": "vendor",
"if2": "normalizedId.includes('/src/core/')",
"return2": "core",
"if3": "normalizedId.includes('/src/ui/')",
"return3": "ui",
"if4": "normalizedId.includes('/src/layer/preset/bar/')",
"return4": "layers-bar",
"if5": "normalizedId.includes('/src/layer/preset/line/')",
"return5": "layers-line",
"if6": "normalizedId.includes('/src/layer/preset/pie/')",
"return6": "layers-pie",
"if7": "normalizedId.includes('/src/layer/preset/scatter/')",
"return7": "layers-scatter",
"if8": "normalizedId.includes('/src/layer/preset/funnel/')",
"return8": "layers-funnel",
"if9": "normalizedId.includes('/src/layer/preset/gauge/')",
"return9": "layers-gauge",
"if10": "normalizedId.includes('/src/layer/preset/radar/')",
"return10": "layers-radar",
"if11": "normalizedId.includes('/src/layer/preset/tree/')",
"return11": "layers-tree",
"if12": "normalizedId.includes('/src/layer/preset/treeMap/')",
"return12": "layers-treemap",
"if13": "normalizedId.includes('/src/layer/preset/hxKLine/')",
"return13": "layers-hxkline",
"if14": "normalizedId.includes('/src/insight/')",
"return14": "insight",
"if15": "normalizedId.includes('/src/hook/')",
"return15": "hook"
},
"plugins": [
{
"terser": {
"mangle": {
"safari10": true,
"reserved": []
}
}
},
{
"visualizer": {
"sourcemap": true,
"open": false,
"gzipSize": true,
"brotliSize": false,
"filename": "path.join(esmOutputDir, 'stats.html')"
}
}
]
}
优点
- 单个 JS 大幅变小,按图层/依赖做长期缓存,二次访问快
- 改动集中在构建层,业务 API 基本不变
代价
- 仅适配支持 ESM 的浏览器(现代浏览器 OK);如需兼容老环境,保留现有 UMD 单包作为 fallback
改动内容
- 在
rollup.config.js增加BUILD_TARGET=cdn-esm分支,新增目录输出配置,开启manualChunks分包与hash命名 - 新增分包:
vendor、core、ui、以及各图层家族(layers-bar、layers-line、layers-pie、layers-scatter、layers-funnel、layers-gauge、layers-radar、layers-tree、layers-treemap、layers-hxkline),以及insight、hook - 目录输出的类型声明邻近文件不再生成(保留
types/主入口),避免和 hash 文件冲突 - 在
package.json新增脚本build:cdn:esm产出 ESM 分包
产物
index-797ee56d.jschunk-vendor-23e6e24c.jschunk-core-1fd4f227.jschunk-ui-3f4e58de.jschunk-layers-*.js(各图层家族)chunk-insight-*.js、chunk-hook-*.jsstats.html(体积分布报告)
使用方式(现代浏览器推荐)
通过 <script type="module" src="/aigc/${version}/esm/index-*.js"></script> 引入,子 chunk 将按需自动请求。
如何微调分包边界(如将较大的 core 再拆分)
- 思路:在
rollup.config.js的productionEsmDirConfig中细化manualChunks,把core再按子域拆分,例如:core-runtime: 运行时基础(src/core/api.ts,src/core/extension.ts,src/core/renderer/**)core-dataprocessor: 数据处理(src/core/dataProcessor/**)core-standardchart: 标准图主视图(src/core/MainStandardChartView.ts及其相关)core-normalview: 常规视图core-hxkline: K 线主/副图视图
- 具体做法(替换你现有的
manualChunks回调,保留已有layer/ui/vendor规则):- 匹配路径再返回更细的 chunk 名称,示例逻辑:
- 路径包含
/src/core/dataProcessor/→core-dataprocessor - 路径包含
/src/core/MainStandardChartView或StandardChart相关 →core-standardchart - 路径包含
/src/core/MainNormalView→core-normalview - 路径包含
/src/core/MainHXKLineView或SecondarySimpleKLineView→core-hxkline - 其他
/src/core/→core-runtime
- 路径包含
- 匹配路径再返回更细的 chunk 名称,示例逻辑:
- 调整后再跑一次
build:cdn:esm,看stats.html中core-*体积分布,继续微调