想象你正站在老房子客厅里,面前是堆了五年的杂物:过期的杂志、落灰的哑铃、再也不会穿的牛仔裤。要搬到新家,你不可能全带走——行李箱就那么大,唯一聪明的做法是分类收纳、只带必需品、抽干空气再封箱。前端工程师发布一个 npm 库,做的正是同一件事:把散落在各个文件里的 JavaScript 模块,用 Rollup 这个“金牌搬家公司”打包成一个体积小、加载快、对使用方友好的包裹。ESM 模块规范是每个收纳箱外贴的清单;Tree-shaking 是把三年没穿的大衣留在老房子;同时输出 ESM 和 CommonJS 两种格式,好比同款商品做了罐装和袋装两种包装;按需加载是把大衣柜拆成板件随取随用;最后再配上真空压缩——一个干净利落的库就出炉了。这篇文章就沿着这条“搬家”主线,把 Rollup 打包库的核心技巧一件件拆给你看。

岗位关联

对 AI 从业者而言,掌握这些打包优化技巧,意味着你能交付体积更小、加载更快的 AI 前端组件与可视化库,直接提升库的下载量、社区采用率和开发者口碑。

目录
  1. 可视化演示
  2. 模块化演进:从杂物间到标准收纳
  3. Rollup优势:为什么库打包要选它
  4. Tree-shaking:只带需要的行李
  5. 双格式输出:一样的货,两种包装
  6. 按需加载:拆成小件随取随用
  7. 体积优化:真空压缩轻装上阵
  8. 实战配置:搬家公司的作业手册
  9. AI 应用场景
  10. Python 演示
  11. 常见误区
  12. 面试题

可视化演示

动态演示Rollup打包与ESM模块的核心机制,通过基础/核心/应用/扩展四种模式展示关键概念与交互流程。

本节配有交互动效(进入视口后自动加载,约 3KB)。

Rollup打包与ESM模块:Roll Rollup核心定位与ESM优 Tree Shaking与静态 核心机制可视化 Rollup打包与ESM模块整体架构与关键组件

模块化演进:从杂物间到标准收纳

你站在老房子客厅中央,满地都是袜子、过期杂志和落灰的健身器材。十几年前的前端世界就是这间客厅——所有脚本一个接一个塞进全局作用域,谁都能碰到谁的东西,稍不留神就相互踩脚。这种“全局混沌”让代码根本没法安全地搬进新家。后来社区从 Node 借来了 CommonJS:每个文件是一间上锁的小房间,require() 是敲门取物。但它是运行时加载,搬家公司得真走进去翻,没法提前知道箱子里装着什么。

随着浏览器原生支持 ESM(ES Module),局面彻底改变:import/export 写在文件顶部,像每个收纳箱外贴的透明清单,搬家公司扫一眼就知道里面装了哪些导出,不需要打开箱子。CommonJS 的 module.exports 在运行期才确定,而 ESM 的静态声明让解析器在编译期就能构建出完整依赖图。npm 包管理器则像小区物业,负责把标准化的收纳箱分发到每个项目。从“全屋敞门”到“上锁房间”再到“门口挂清单”,模块化的每一步都在降低搬运的不可预知性。

清单贴好,搬家就赢在了起跑线。有了可静态分析的 ESM,接下来只需选对搬家公司——它对打包结果的影响超乎你的想象。

Vtotal=V1+V2+⋯+Vn=∑i=1nViV_{\text{total}} = V_1 + V_2 + \cdots + V_n = \sum_{i=1}^{n} V_i
打包前的总代码量等于所有模块体积之和。模块化时代我们已经能数清每个箱子的份量,这是后续所有优化得以发生的前提。
  1. 单个模块体积 V_i第 i 个模块自身的代码量。模块就像收纳箱,体积就是它的份量。
    Vi\displaystyle V_i
  2. 求和符号 Σ把从第 1 个到第 n 个模块的体积逐一加总。
    ∑i=1n\displaystyle \sum_{i=1}^{n}
  3. 总搬运量 V_totaln 个模块的总代码体积,也就是打包动作发生前的原始总量。
    Vtotal\displaystyle V_{\text{total}}

Rollup优势:为什么库打包要选它

你找到了两家搬家公司。Webpack 是“全能搬家公司”,什么都能搬——图片、CSS、字体、脚本一锅端,非常适合复杂的网页应用。但搬家后打开箱子,你会发现每个箱子里都塞了一本“通用搬运手册”(运行时 runtime)。如果你只搬几个书箱(一个 npm 库),这本手册就显得格外笨重。Rollup 是“精品搬家公司”,专精 JS 库打包,它基于 ESM 静态依赖图一次成型,输出就是干净的模块串联:所见即所得,没有多余胶水代码。

库的消费者(webpack、Vite、Node)各自都有完整的打包能力,库本身不需要再携带运行时引导代码。这正是 Rollup 在库场景胜出的核心:Webpack 为应用而生,擅长处理各种资源与复杂分包;Rollup 为库而生,产物整洁、无 runtime、天然支持 tree-shaking。两者不是替代关系,而是“库用 Rollup、应用用 Webpack/Vite”的黄金分工。由于 Rollup 处理的资源类型少、依赖图静态明确,构建速度通常也更快。

选对了承运商,行李已经在安心上路。但真正让箱子肉眼可见变轻的,是接下来的决定:哪些东西根本不搬。

R=VrollupVwebpackR = \frac{V_{\text{rollup}}}{V_{\text{webpack}}}
体积比 R 是同一份库分别用 Rollup 和 Webpack 打包后的产物大小之比。R 小于 1 说明 Rollup 输出更精简,库场景下这个差距通常相当明显。
  1. V_rollupRollup 打包后的产物体积,通常更小更干净。
    Vrollup\displaystyle V_{\text{rollup}}
  2. V_webpackWebpack 打包后的产物体积,包含额外的运行时胶水。
    Vwebpack\displaystyle V_{\text{webpack}}
  3. 比值 R两个体积之比。R 越小于 1,说明 Rollup 的“精简优势”越大。
    R\displaystyle R

Tree-shaking:只带需要的行李

装车时,妻子从柜子里翻出一件三年没穿的大衣和一台一次没用的榨汁机。打包代码也一样——一个库往往导出很多函数,但使用者真正触达的只是其中一部分,剩下的就是死代码(dead code)。Tree-shaking(摇树)这个词源于打包器:像摇苹果树,熟透的果子(被 import 的代码)掉进筐,枯枝烂叶(没被引用的导出)留在树上。Rollup 为什么能摇树?因为 ESM 的 import/export 是静态的,依赖图在编译期就是一张透明的搬运清单。

从技术机制看,Rollup 从入口模块出发,做一次图遍历(本文 Python 演示就是一个极简模拟),标记所有可达代码,不可达的导出整个不输出。精准度可以到符号级:同一个模块里用了 formatURL,没被引用的 randomText 也能被单独摇掉。但有个关键前提——模块不能有副作用(side effect):比如模块一加载就给全局对象挂属性、立即执行 console.log。打包器出于安全会保守地保留这类模块。解决办法是在 package.json 里声明 "sideEffects": false 表示全库无副作用,或给纯函数调用加上 /*#__PURE__*/ 注释,告诉打包器“这箱东西纯得很,没人用就放心扔”。

杂物留在老房子,箱子肉眼可见地轻了。接下来是包装问题:同样的货,怎么让不同习惯的“使用方”都方便打开?

Vused=∑m∈UVmV_{\text{used}} = \sum_{m \in U} V_m
打包后的有效体积等于“从入口可达的模块集合 U”中各模块体积之和。Tree-shaking 的本质就是把公式里的求和范围从“全部模块”缩小成“可达模块”。
  1. 可达模块集合 U从入口通过 import 能抵达的所有模块,如同搬家公司实际会碰到的箱子。
    U\displaystyle U
  2. 模块体积 V_m集合 U 中第 m 个模块的代码量。
    Vm\displaystyle V_m
  3. 有效体积 V_used对 U 中所有模块体积求和——不可达的死代码已经不在求和范围内。
    Vused\displaystyle V_{\text{used}}

双格式输出:一样的货,两种包装

同一种酱料,超市会做罐装和袋装:现代厨房开罐即用,露营族撕袋即食。ESM 和 CommonJS 就是 JS 世界的两种包装规格。ESM 是浏览器和现代构建工具的原生格式,支持静态分析和 tree-shaking;CJS 是 Node 老项目的主场,历史上大量 npm 包基于它。你的库最好同时提供两份,让不同模块系统的使用方各取所需。Rollup 用 output 数组一次产出两份文件:dist/index.esm.js 和 dist/index.cjs.js,配置 format 分别为 'esm' 和 'cjs'。

但产出两份文件只是第一步,真正的关键在 package.json 的 exports 字段——货架上的价签。它精确控制“谁在什么条件下看到哪个文件”:当使用方用 import 语法(或现代构建工具)引入时,指向 ESM 文件;当使用方用 require() 引入时,指向 CJS 文件。这里要特别警惕“双包危险模式”(dual package hazard):如果两份产物各自持有状态,且某些场景下使用方同时加载了两份,就会出现“同一个库两个实例”的诡异 bug——好比同一款酱料罐装和袋装各倒半包,味道就变了。

同样的馅料,两种面皮都包得妥帖。解决了包装兼容,下一步是让大包裹能“拆着搬”——这就是按需加载。

entry(f)={dist/index.esm.jsf=importdist/index.cjs.jsf=require\text{entry}(f) =\begin{cases}\text{dist/index.esm.js} & f = \text{import} \\\text{dist/index.cjs.js} & f = \text{require}\end{cases}
入口映射是一个按模块系统类型分流的分段函数:import 走 ESM 产物,require 走 CJS 产物,各回各家。
  1. 输入条件 f使用方所用的模块系统类型,只有 import 和 require 两种取值。
    f\displaystyle f
  2. ESM 分支当 f 为 import 时,映射到 dist/index.esm.js。
    dist/index.esm.js\displaystyle \text{dist/index.esm.js}
  3. CJS 分支当 f 为 require 时,映射到 dist/index.cjs.js。
    dist/index.cjs.js\displaystyle \text{dist/index.cjs.js}

按需加载:拆成小件随取随用

搬家车一次装不下整柜家具,于是把大衣柜拆成板件,进了新家想用哪块再拼哪块。库的场景完全一样:使用方往往只用到你库的一小部分功能,如果所有代码都塞进一个文件,首屏就得下载全部体积。动态 import() 语法让“用到时再加载”成为可能:使用方调用某个子功能时,才去拉取对应的 chunk。Rollup 识别到动态 import() 后会自动做代码分割(code splitting),把原本的一个大包拆成多个 chunk 文件。

技术上有几个关键点:第一,output 配置里可以控制 chunk 的命名与分包策略,让产物文件名可预测、可缓存;第二,分包粒度需要拿捏——拆得太碎,几十个 2KB 的小 chunk 反而增加请求开销;第三,库的按需加载常见做法是入口只暴露主功能,低频子功能通过动态 import 渐进加载,并在文档里引导使用方配合现代构建工具的 tree-shaking 做按需引用。最终目标是让使用方的首屏只加载“当前需要的板件”,而不是整箱家具。

大件拆成小件,新家随取随用。最后一步,是给每个小件抽掉空气、压到最实。

Tload=∑c∈CScBT_{\text{load}} = \sum_{c \in C} \frac{S_c}{B}
总加载时间等于实际加载的 chunk 集合 C 中每个 chunk 体积除以带宽之和。按需加载的精髓,就是让求和范围 C 尽可能小。
  1. 实际加载集合 C使用方在当前页面真正拉取的 chunk 集合,对应“拆开后实际用到的板件”。
    C\displaystyle C
  2. 单个 chunk 体积 S_c第 c 个 chunk 的文件大小。
    Sc\displaystyle S_c
  3. 带宽 B网络传输速度,分母固定时分子越小,加载越快。
    B\displaystyle B
  4. 总加载时间 T_load所有被加载 chunk 的传输时间之和,C 越小这个值越小。
    Tload\displaystyle T_{\text{load}}

体积优化:真空压缩轻装上阵

收纳师拿出真空压缩袋,把羽绒服压成饼干大小。代码也有“抽空气”的手段——压缩与混淆(minify):去掉空格和注释、把长变量名缩成单字母。压缩通常是打包产物的最后一道工序,能把体积再砍掉一半以上。但对 npm 库来说,“谁压缩”很有讲究:库代码通常交给使用方的构建工具统一压缩,库作者只需保证源码整洁、便于 tree-shaking。如果你把库预先压成“真空饼干”,使用方的工具反而无法再次摇树,sourcemap 也会错位,出了问题排错极难。

另一种更重要的瘦身是外置依赖(external):把 react、lodash 等大件声明为“不打包”。这些依赖应该在使用方项目里统一安装和提供,否则会出现两份 React 实例、体积膨胀、版本冲突一堆问题。具体做法是在 package.json 里用 peerDependencies 声明“本库需要 react 作为同伴依赖”,而不是塞进 dependencies。库体积的真正度量标准是 gzip 体积——网络传输实际计费的“快递重量”,业界常以“gzip 后多少 KB”作为库是否体面的指标。

抽掉空气的行李,扛起来一身轻。所有技巧都备齐了,最后把它们固化成一张可以反复使用的派工单吧。

Vfinal=Vused×c×(1−r)V_{\text{final}} = V_{\text{used}} \times c \times (1 - r)
最终用户下载体积 = Tree-shaking 后的有效体积 × 压缩系数 × (1 − gzip 压缩率)。每一步优化都在让这个乘积变小。
  1. 有效体积 V_used经过 Tree-shaking 后还留在产物里的代码量。
    Vused\displaystyle V_{\text{used}}
  2. 压缩系数 cminify 压缩后体积与原体积之比,通常小于 1。
    c\displaystyle c
  3. gzip 压缩率 r网络传输层 gzip 能再压掉的比例。
    r\displaystyle r
  4. 最终体积 V_final三者连乘后的结果,也就是用户真实下载的字节数。
    Vfinal\displaystyle V_{\text{final}}

实战配置:搬家公司的作业手册

搬家公司会给每位客户一张派工单:从哪个房间开始搬、带哪些工具、送到哪几个地址。Rollup 的配置文件 rollup.config.js 就是这张派工单——它是整个打包流程的指挥中枢。一份典型的库配置长这样:input 指定打包入口文件(从哪个房间开始);output 数组同时输出 ESM 和 CJS 两种格式(送到几个地址);plugins 挂载压缩、别名、声明文件生成等工具;external 列出外置依赖清单(哪些大件不搬)。

完整发布流程是一条标准流水线:清理 dist 目录 → 执行 rollup -c 打包 → 类型检查与声明文件生成 → 用 publint、arethetypeswrong 等工具检查产物 → 最后 npm publish。发布检查重点看三件事:exports 字段是否完整映射、类型入口是否存在、是否存在双包危险。配置项之间环环相扣:external 决定外置清单,output 决定格式与文件名,plugins 决定产物质量——任何一环配错,产物体积或兼容性都会出问题。

手册在手,下次搬家照章办事。从杂物间到标准收纳,从选对搬家公司到真空压缩——你现在已经完全有能力,做自己库的金牌搬家公司了。

outputs=F(config)=[Oesm, Ocjs]\text{outputs} = F(\text{config}) = [O_{\text{esm}},\ O_{\text{cjs}}]
构建过程 F 吃进一份配置,吐出多个产物。Rollup 的优雅之处在于:一份派工单,可同时生成 ESM 与 CJS 两份交付物。
  1. 配置对象 configinput、output、plugins、external 等选项的集合。
    config\displaystyle \text{config}
  2. 构建过程 FRollup 依据配置执行的读取、摇树、打包、插件处理全套流程。
    F\displaystyle F
  3. 产物数组 outputs输出结果,常同时包含 O_esm 与 O_cjs 两种格式文件。
    [Oesm, Ocjs]\displaystyle [O_{\text{esm}},\ O_{\text{cjs}}]

AI 应用场景

AI辅助库体积体检

阶段:性能分析

解决的问题:传统做法靠人工 review 和手动翻打包产物找体积问题,效率低、易遗漏。AI 可以扫描库源码与构建产物,自动识别未使用导出、重复代码、可外置依赖,并生成带优先级的优化建议清单,相当于给搬家行李做一次 AI 智能盘点。

具体案例:某团队开发一个图表库,用 LLM 分析 Rollup 构建日志与产物依赖图,自动发现 tools 目录下三个从未被引用的工具函数、误入 dependencies 的 lodash,并建议改用 lodash-es 按需引入。按建议优化后,产物体积从 120KB 降到 48KB,gzip 后仅 18KB。

自然语言生成 rollup.config.js

阶段:自动生成

解决的问题:Rollup 配置项繁多(output、external、plugins),新手容易配错,老手也常翻文档。用 LLM 理解用户意图——“我要输出 ESM 和 CJS,外置 react,开启压缩”——直接生成合规的配置文件,相当于用大白话给搬家公司下派工单。

具体案例:某公司内部 Copilot 结合工程规范与历史配置样本,将一句“生成一个双格式输出的库配置”指令转换为完整的 rollup.config.js,并自动校验 exports 字段合法性与双包风险,省去半小时的手写调试时间。

ESM 迁移助手

阶段:代码迁移

解决的问题:老库仍在使用 CommonJS,享受不到 ESM 静态分析和 tree-shaking 红利,迁移成本又高。AI 可以静态分析 require/module.exports 调用关系,自动改写为 import/export,并保持行为一致,让老库重新获得摇树能力——这就是把杂物间改造成标准收纳房。

具体案例:用 AST + LLM 流水线处理一个拥有 40 个模块的老工具库,将 CJS 改写为 ESM 后配合 Rollup 打包,产物体积减少 35%,且全部单元测试通过。

Python 演示

python50 行
"""Tree-shaking 极简模拟: 从入口出发做可达性分析""" from collections import deque MODULE_GRAPH = {    "entry": (["main"], [("utils", "formatURL"), ("api", "fetchData")]),    "utils": (["formatURL", "randomText"], [("lib", "pad")]),    "api": (["fetchData"], []),    "lib": (["pad", "sha256"], []),    "debug": (["logTrace"], []),}  def tree_shake(entry):    """返回保留模块集合, 以及各模块中被实际用到的符号表"""    used_modules = set()    used_symbols = {}    queue = deque([entry])     while queue:        mod = queue.popleft()        if mod in used_modules:            continue        used_modules.add(mod)        exports, imports = MODULE_GRAPH[mod]        syms = used_symbols.get(mod, set())        for sym in exports:            syms.add(sym)        used_symbols[mod] = syms        for dep_mod, dep_sym in imports:            if dep_mod not in used_symbols:                used_symbols[dep_mod] = set()            used_symbols[dep_mod].add(dep_sym)            if dep_mod not in used_modules:                queue.append(dep_mod)    return used_modules, used_symbols  def main():    used_modules, used_symbols = tree_shake("entry")    print("保留的模块:", sorted(used_modules))    print("各模块记录到的符号(导出+被引用):")    for mod in sorted(used_symbols):        print(f"  {mod}: {sorted(used_symbols[mod])}")    dead = [m for m in MODULE_GRAPH if m not in used_modules]    print("被剔除的死代码模块:", dead)  if __name__ == "__main__":    main()
目的:用 Python 实现一个极简 Tree-shaking 模拟器——从入口模块出发做广度优先遍历,找出所有可达模块,没被访问到的模块就是会被 Rollup 摇掉的死代码。
  1. queue = deque([entry])从入口模块开始的遍历队列,是可达性分析的起点
  2. if mod in used_modules: continue已访问过的模块直接跳过,防止循环依赖导致死循环
  3. exports, imports = MODULE_GRAPH[mod]拆出当前模块的导出清单和引用清单,对应 ESM 的静态声明
  4. used_symbols[dep_mod].add(dep_sym)记录依赖模块中被真正用到的符号,这是符号级摇树的依据
  5. if dep_mod not in used_modules: queue.append(dep_mod)把未访问的依赖模块放进队列,继续扩散分析
  6. dead = [m for m in MODULE_GRAPH if m not in used_modules]遍历结束后,从未被访问到的模块就是死代码,打包时整块剔除

常见误区

误区既然 Rollup 会摇树,我只要把代码丢进去,所有没用到的部分都会自动消失,什么都不用管。

正解Tree-shaking 依赖 ESM 的静态结构,CommonJS 的 require 是运行时动态调用,根本无法静态分析。而且有副作用的模块会被打包器保守保留——搬家公司只认清单,但如果你没给箱子贴“纯物品”标签,它不敢扔,万一里面藏着定时炸弹呢?正确做法是声明 sideEffects: false 并善用 /*#__PURE__*/ 注释。

为何常见:打包器 demo 展示的摇树效果过于惊艳,让新手把“优化能力”当成了“免维护承诺”。

误区发布前必须把产物体积压到最小,最好先用 Terser 压缩好再上传 npm,用户下载时越小越好。

正解库通常交给使用方的构建链统一压缩。预压缩的产物无法被上层再次 tree-shaking,sourcemap 对不上还增加排错难度。你应该交付“洗好切好的净菜”而不是“真空熟食”——保留未压缩的 ESM 源码作为主入口,必要时额外提供压缩变体供 CDN 直接引用。

为何常见:应用打包时压缩是标配操作,人们把应用场景的经验直接平移到了库场景。

误区用 Rollup 的 `output` 数组同时输出两种格式,所有使用方的兼容问题都解决了。

正解还差关键一步:配置 exports 条件映射,并且警惕“双包危险模式”。当使用方某些场景同时加载 ESM 和 CJS 两份产物,会产生两个模块实例——React 库双实例会导致 hooks 状态错乱。罐装和袋装虽然是同一款酱,但两罐同时下锅,味道就过了。解决思路:要么只保留 ESM(现代浏览器和 Node 16+ 都已支持),要么确保两份格式不持有模块顶层可变状态。

为何常见:看到 dist 里有两个文件就以为大功告成,忽略了“同一份逻辑被加载两次”这种运行时层面的问题。

误区库应该自带全部依赖,用户只装我一个小包就能开箱即用,体验最好。

正解把 react、lodash 这类常见依赖打进包内,会导致使用方项目出现两份依赖实例、体积膨胀和 peer 版本冲突。正确做法是在 Rollup 配置里声明 external,并在 package.json 用 peerDependencies 声明同伴依赖。就像新房本来就配好冰箱,你硬要再搬一台进去,厨房会挤得站不下人。

为何常见:直觉上“一键安装即用”很友好,没意识到依赖去重和版本协同的复杂性远超想象。

误区只要代码里写了 `import()`,库的按需加载就自动完成了,用户一定只加载需要的部分。

正解动态 import() 只是代码分割的触发器,后续还有 chunk 命名、分包粒度控制、避免循环依赖、文档引导等一系列工程化工作。如果对每个函数都做动态 import,会产生几十个 2KB 的小 chunk,请求数爆炸反而拖垮性能。拆小件只是第一步,小件还要贴好标签、放到对应楼层,才算真正方便。

为何常见:把单个 API 的“用法”当成了完整“策略”,缺少工程化全局视角。

面试题

Q1. 关于 Rollup 和 Webpack,下列说法正确的是?

单选题 · easy

  • A.Rollup 只能打包纯 JS 文件
  • B.Webpack 更适合打包 NPM 库
  • C.Rollup 打包库输出更干净精简← 正确答案
  • D.Webpack 必然比 Rollup 体积小

答案:C。Rollup 专精 JS 库打包,产物无运行时胶水、更干净;Webpack 为复杂应用而生,擅长处理各类资源。两者分工不同,A 说“只能”过于绝对,B 说反了,D 的“必然”不成立,所以选 C。

Q2. Tree-shaking 能生效的前提条件是什么?

单选题 · easy

  • A.模块使用 CommonJS 的 require
  • B.模块使用 ESM 静态 import 导出← 正确答案
  • C.代码必须预先经过压缩处理
  • D.必须使用 webpack 进行构建

答案:B。Tree-shaking 依赖 ESM 的静态结构——import/export 写在文件顶部,编译器能提前构建依赖图。CommonJS 的 require 是运行时动态的,无法静态分析;压缩和 webpack 都不是前提,故选 B。

Q3. 在 package.json 中声明 "sideEffects": false 的含义是什么?

单选题 · mid

  • A.打包时禁用所有插件工具
  • B.所有模块无副作用可安全摇树← 正确答案
  • C.该库只能在浏览器环境运行
  • D.强制要求使用 ESM 模块格式

答案:B。该声明告诉打包器“这个包的模块都没有副作用”,因此没有引用的导出可以被安全删除。它不涉及插件、运行环境或格式强制,A、C、D 都是对副作用的误读,故选 B。

Q4. 下列哪些是 ESM 相比 CommonJS 的优势?

多选题 · mid

  • A.静态结构便于工具分析优化← 正确答案
  • B.天然支持 Tree-shaking 摇树← 正确答案
  • C.模块同步加载后立即执行
  • D.标准语法支持动态 import()← 正确答案

答案:ABD。A 和 B 是 ESM 静态声明的直接红利;D 的 import() 是 ESM 标准中的动态导入语法。C 恰好是 CommonJS 的特点(同步执行),不属于 ESM 优势,故不选。

Q5. package.json 的 exports 字段主要作用是什么?

单选题 · mid

  • A.声明包的许可证与作者信息
  • B.按条件映射模块入口文件← 正确答案
  • C.限制包的最大体积上限
  • D.声明依赖的最低版本要求

答案:B。exports 字段精确控制不同条件(如 import/require)下模块入口的映射路径,是双格式输出的“货架价签”。许可证、体积限制、依赖版本分别由其他字段负责,故选 B。

Q6. 关于双包危险模式(dual package hazard),下列说法正确的是?

多选题 · hard

  • A.同时存在两套模块系统代码← 正确答案
  • B.使用方可能加载两份模块实例← 正确答案
  • C.引发多实例与状态不一致问题← 正确答案
  • D.仅在浏览器环境才会出现

答案:ABC。双包危险指 ESM 和 CJS 两份产物被同一使用方同时加载,形成两套实例和两份状态,这在 Node 和浏览器环境都可能发生。D 说“仅浏览器”错误,故选 A、B、C。

Q7. /*#__PURE__*/ 注释在 Rollup 中的作用是什么?

单选题 · hard

  • A.删除代码中所有 console 语句
  • B.标注函数无副作用可被摇掉← 正确答案
  • C.开启浏览器 GPU 硬件加速
  • D.跳过 ESLint 语法检查规则

答案:B。/*#__PURE__*/ 标记一个函数调用是“纯”的——若返回值未被使用,该调用可以安全删除。它帮助 Tree-shaking 消除有副作用的误判,与删除日志、GPU 加速、ESLint 无关,故选 B。

Q8. 动态 import() 配合代码分割的主要收益是什么?

单选题 · mid

  • A.加快项目的编译构建速度
  • B.首包变小实现按需加载← 正确答案
  • C.减少开发者的代码量
  • D.自动修复运行时的 Bug

答案:B。动态 import() 触发代码分割,将大包拆成若干 chunk,使用方首屏只加载主 chunk,用到子功能时才拉取对应 chunk。它不直接影响编译速度、代码量或 Bug 修复,故选 B。

Q9. 库打包时在 external 中声明 react 的主要目的是什么?

单选题 · extreme

  • A.把依赖打包进最终产物
  • B.让使用方自行提供该依赖← 正确答案
  • C.禁止代码中使用该依赖
  • D.自动压缩该依赖的代码

答案:B。external 声明让 Rollup 不打进 react,改由使用方项目统一提供,并通过 peerDependencies 声明配套版本,避免双实例与体积膨胀。它不是禁用、不是压缩、更不是打进产物,故选 B。

Q10. 下列哪些手段能有效减小 npm 库的体积?

多选题 · easy

  • A.用 Tree-shaking 消除死代码← 正确答案
  • B.压缩混淆减小产物体积← 正确答案
  • C.外置依赖避免重复打包← 正确答案
  • D.在文档中增加更多注释

答案:ABC。Tree-shaking 删死代码、压缩混淆抽掉“空气”、外置依赖把大件留给使用方,三管齐下能显著瘦身。D 写文档不影响产物体积(注释在压缩时还会被删掉),故不选。