搜索文章

输入关键词开始搜索

ESM分包

程序设计#JavaScript#数据可视化#AI

ESM分包配图

需求来源

我们最近要开发一个组件给业务方使用,网页对于性能有较高的需求。而我们的组件代码量比较大,会导致组件的 CDN 比较大,加载耗时较长。因此我们需要对组件进行分包处理,将原本较大的 CDN 拆分为多个相对较小的 CDN。

之前的项目打包时支持导出 ESM 和 UMD 两种形式。但 UMD 格式是为了兼容不同浏览器模块化规范的处理,其产物没有包和模块的概念,不支持分包。因此我们只能选择 ESM 分包方案。

ESM 与 UMD 的区别

  • ESM 是官方标准,提供静态导入/导出、原生异步 import(),是实现代码分割的根本底层
  • UMD 是兼容包装,把库封装成一个同步闭包,让它能够在 AMD、CommonJS、全局三种环境中使用
  • 关系:UMD 常常是 ESM → 打包 → UMD 的产物;UMD 失去 ESM 的静态特性后,就不再具备代码分割的先决条件

为什么 UMD 天生不支持代码分割

  1. 设计为一次性同步加载,没有运行时的 Chunk Registry
  2. 没有静态依赖信息,打包工具只能把它当作黑盒
  3. 代码分割需要共享闭包/模块缓存,而 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 命名
  • 新增分包:vendorcoreui、以及各图层家族(layers-barlayers-linelayers-pielayers-scatterlayers-funnellayers-gaugelayers-radarlayers-treelayers-treemaplayers-hxkline),以及 insighthook
  • 目录输出的类型声明邻近文件不再生成(保留 types/ 主入口),避免和 hash 文件冲突
  • package.json 新增脚本 build:cdn:esm 产出 ESM 分包

产物

  • index-797ee56d.js
  • chunk-vendor-23e6e24c.js
  • chunk-core-1fd4f227.js
  • chunk-ui-3f4e58de.js
  • chunk-layers-*.js(各图层家族)
  • chunk-insight-*.jschunk-hook-*.js
  • stats.html(体积分布报告)

使用方式(现代浏览器推荐)

通过 <script type="module" src="/aigc/${version}/esm/index-*.js"></script> 引入,子 chunk 将按需自动请求。

如何微调分包边界(如将较大的 core 再拆分)

  • 思路:在 rollup.config.jsproductionEsmDirConfig 中细化 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/MainStandardChartViewStandardChart 相关 → core-standardchart
      • 路径包含 /src/core/MainNormalViewcore-normalview
      • 路径包含 /src/core/MainHXKLineViewSecondarySimpleKLineViewcore-hxkline
      • 其他 /src/core/core-runtime
  • 调整后再跑一次 build:cdn:esm,看 stats.htmlcore-* 体积分布,继续微调