Chunk:三种产物的打包逻辑
| 维度 | v1(原始版本) | v2(当前版本) | |------|---------------|---------------| | Webpack 版本 | Webpack 5.x 通用 | Webpack 5.107 精确对齐 | | seal 阶段流程 | 4 步概览(创建 Entry → visitModules → connectChunkGroups → cleanup) | 5 步完整生命周期(Entry 创建 → Module 遍历关联 → ChunkGraph 构建 → 优化阶段 → 产物生成) | | 图表 | 外链图片(4 张) | 3 张 Mermaid 原生流程图(可交互渲染) | | Chunk 概念 | Entry/Async/Runtime 三种 Chunk | 扩展为 Initial/Async/Runtime + CSS Chunk 四种产物 | | 示例深度 | 单 entry 基础示例 | 多 entry + code splitting 完整推演 | | 新增内容 | - | output.chunkFilename / cssFilename、deterministic IDs、experiments.css、Chunk.files 与 auxiliaryFiles 区分 | | 优化阶段 | 仅提及 SplitChunksPlugin | 详细展开 optimizeChunks → optimizeModules → optimizeCodeGeneration 三层优化链路 | | 代码引用 | Webpack 早期源码片段 | 对齐 webpack@5.107 最新源码路径与逻辑 |
在上一篇文章《Dependency Graph:如何管理模块间依赖?》中,我们已经详细讲解了「构建」阶段如何从 Entry 开始逐步递归读入、解析模块内容,并最终构建出模块依赖关系图 —— ModuleGraph 对象。本文我们继续往下,讲解在接下来的「封装」(seal)阶段,如何根据 ModuleGraph 内容组织 Chunk,并进一步构建出 ChunkGroup、ChunkGraph 依赖关系对象的完整主流程。
核心认知先行:Chunk 是产物的组织单位。一个 Chunk 最终对应一个或多个输出文件(JS/CSS/资源文件),而 Chunk 本身是若干 Module 的集合容器。理解 Chunk,就是理解 Webpack 如何决定「哪些模块被打包到哪个文件」。
主流程之外,我们还会详细拆解几个关键概念:
Chunk、ChunkGroup、ChunkGraph对象分别是什么?互相之间存在怎样的层级与交互关系?- Webpack 内置的三种(实际四种)分包规则及其产物特征。
- seal 阶段完整的 Chunk 生命周期:从创建到优化的全链路。
output.chunkFilename、deterministic module/chunk IDs等高级配置对产物的影响。
一、seal 阶段总览:Chunk 的完整生命周期
在《Init、Make、Seal:真正读懂 Webpack 核心流程》中,我们已经介绍了 Webpack 底层构建逻辑大体上可以划分为:「初始化 → 构建 → 封装」三个阶段:
┌──────────┐ ┌──────────┐ ┌──────────┐
│ Init │ → │ Make │ → │ Seal │
│ 初始化 │ │ 构建 │ │ 封装 │
└──────────┘ └──────────┘ └──────────┘
│ │
▼ ▼
ModuleGraph ChunkGraph
(模块依赖图) (产物组织图)其中:
- Make(构建)阶段:分析模块间的依赖关系,建立
ModuleGraph - Seal(封装)阶段:根据
ModuleGraph将模块分配进Chunk,构建ChunkGraph,经过一系列优化后最终产出文件
Seal 阶段是 Chunk 诞生的唯一场所。下面这张 Mermaid 图完整展示了 Chunk 从创建到最终产出的全过程:
下面我们对每个步骤逐一深入剖析。
二、步骤详解:Chunk 的创建、关联与优化
步骤 ①:遍历 entries —— 为每个 entry 创建 Chunk 与 Entrypoint
调用 compilation.seal() 后,Webpack 首先遍历 entry 配置项,为每一个入口执行以下操作:
// webpack@5.107 lib/Compilation.js — seal() 方法关键片段
class Compilation {
seal(callback) {
const chunkGraphInit = new Map();
// 遍历入口模块列表
for (const [name, { dependencies, includeDependencies, options }] of this.entries) {
// 1. 为每一个 entry 创建对应的 Chunk 对象
const chunk = this.addChunk(name);
// 2. 为每一个 entry 创建对应的 Entrypoint 对象(Entrypoint 继承自 ChunkGroup)
const entrypoint = new Entrypoint(options);
// 3. 关联 Chunk 与 ChunkGroup(双向绑定)
connectChunkGroupAndChunk(entrypoint, chunk);
// 4. 遍历 entry 的 Dependency 列表
for (const dep of [...this.globalEntry.dependencies, ...dependencies]) {
// 记录入口依赖的来源信息
entrypoint.addOrigin(null, { name }, dep.request);
// 通过 moduleGraph 找到 dependency 对应的 Module
const module = this.moduleGraph.getModule(dep);
if (module) {
// 在 ChunkGraph 中记录「入口模块 ↔ Chunk」的映射关系
chunkGraph.connectChunkAndEntryModule(chunk, module, entrypoint);
}
}
}
// 处理 runtime chunk 配置
for (const [name, { dependencies, options, runtime }] of this.entries) {
if (runtime) {
// 如果 entry 配置了 runtime 属性,创建独立的 Runtime Chunk
const runtimeChunk = this.addChunk(runtime);
// 将 runtimeChunk 关联到 entrypoint
entrypoint.setRuntimeChunk(runtimeChunk);
}
}
// 调用 buildChunkGraph,进入下一步骤
buildChunkGraph(this, chunkGraphInit);
}
}这一步完成后形成的数据结构:
关键点:此时 Chunk 中只有入口模块本身,尚未包含其依赖的子模块。
步骤 ②:处理 Runtime Chunk 提取
如果 entry 配置中声明了 runtime 属性,Webpack 会在此阶段为运行时代码创建独立 Chunk:
// 示例配置
module.exports = {
entry: {
index: { import: "./src/index", runtime: "solid-runtime" },
home: { import: "./src/home", runtime: "solid-runtime" },
},
};当多个 entry 共享同一个 runtime 名称时,它们的运行时代码会被合并到同一个 Runtime Chunk 中,避免重复打包。这是 Webpack 5 推荐的多 entry 运行时优化方案。
步骤 ③:buildChunkGraph() —— 构建完整的 Chunk 图结构
这是 seal 阶段最核心的方法,位于 lib/buildChunkGraph.js。它内部依次调用三个关键函数:
③-a:visitModules() —— 遍历 ModuleGraph,分配 Module
visitModules 函数递归遍历 ModuleGraph 中的每个 Module,按照以下规则将其分配到 Chunk:
| Module 类型 | 分配行为 | |------------|---------| | 同步依赖的 Module | 加入当前 Chunk(即所属 entry 的 Chunk) | | 异步依赖的 Module(import() / require.ensure) | 创建新的 ChunkGroup + Chunk,将该 Module 及其同步子模块加入新 Chunk |
伪代码逻辑如下:
// lib/buildChunkGraph.js — visitModules 核心逻辑(简化)
function visitModules(compilation, chunkGraph) {
for (const module of compilation.modules) {
for (const connection of chunkGraph.getModuleConnections(module)) {
const targetModule = connection.module;
if (connection.isTargetActive()) {
if (connection.isAsyncDependency()) {
// 异步依赖 → 创建新 ChunkGroup 和 Chunk
const asyncChunkGroup = new ChunkGroup();
const asyncChunk = compilation.addChunk();
connectChunkGroupAndChunk(asyncChunkGroup, asyncChunk);
// 将异步模块及其同步子模块加入新 Chunk
chunkGraph.connectChunkAndModule(asyncChunk, targetModule);
// 建立 ChunkGroup 间的父子关系
currentChunkGroup.addChild(asyncChunkGroup);
asyncChunkGroup.addParent(currentChunkGroup);
} else {
// 同步依赖 → 直接加入当前 Chunk
chunkGraph.connectChunkAndModule(currentChunk, targetModule);
}
}
}
}
}③-b:connectChunkGroups() —— 建立 ChunkGroup 依赖关系
此方法将上一步产生的 ChunkGroup 之间的父子关系固化到 ChunkGraph 数据结构中,形成完整的依赖拓扑。
③-c:cleanupUnconnectedGroups() —— 清理无效节点
移除没有任何 Module 引用的孤立 ChunkGroup,纯性能优化。
步骤 ④:optimizeChunks —— SplitChunksPlugin 等优化插件介入
buildChunkGraph 完成后,Chunk 的初始结构已经确定。接下来进入优化阶段,各类插件可以修改 Chunk 结构:
// lib/Compilation.js — seal() 中的优化钩子调用顺序
async seal(callback) {
// ... buildChunkGraph 完成 ...
// ① Chunk 级别优化(SplitChunksPlugin 在此处介入)
this.hooks.optimizeChunks.call(this.chunks, this.chunkGroups);
// 此时 SplitChunksPlugin 可以:拆分公共模块为新 Chunk、合并小 Chunk 等
// ② Module 级别优化(ModuleConcatenationPlugin 在此处介入)
this.hooks.optimizeModules.call(this.modules);
// 此时可以将多个 Module 合并为同一个作用域(Scope),减少函数包裹开销
// ③ 代码生成优化
this.hooks.optimizeCodeGeneration.call(this.modules);
// ... 继续后续流程 ...
}重点理解:
SplitChunksPlugin并不参与 Chunk 的初始创建,它是在buildChunkGraph完成后,通过optimizeChunks钩子二次重组 Chunk 结构的。这意味着内置的三种 Chunk 规则(Entry/Async/Runtime)是基础,SplitChunks 是在此基础上进行的启发式优化。
步骤 ⑤:optimizeModules —— Module Concatenation 等优化
此阶段主要进行 Module 级别的优化,最典型的是 ModuleConcatenation(作用域提升):将多个 IIFE 包裹的 Module 合并到同一个函数作用域中,减少运行时的模块查找开销。
步骤 ⑥:产物生成 —— 根据 Chunk 类型输出文件
最终,Webpack 遍历所有 Chunk,根据其类型和配置的 filename 模板,将 Module 内容序列化为文件输出:
// 每个 Chunk 可能产出:
// - chunk.files: 主产物文件列表(如 .js)
// - chunk.auxiliaryFiles: 辅助产物文件列表(如 .map、CSS 文件等)三、Chunk vs ChunkGroup vs ChunkGraph —— 三者关系全景
上述构建过程涉及三个核心数据对象,它们形成了清晰的层级关系:
各对象职责一览
| 对象 | 职责 | 关键属性 | |------|------|---------| | Chunk | Module 的集合容器,产物的直接来源 | id, name, files, auxiliaryFiles, entryModule | | ChunkGroup | Chunk 的分组管理器,维护加载顺序和父子关系 | _chunks[], _parents[], _children[], origins[] | | Entrypoint | 继承自 ChunkGroup,代表一个入口点 | 新增 runtimeChunk 属性 | | ChunkGraph | 全局索引,记录三者间所有映射关系 | getChunkModules(), getModuleChunks(), getChunkParentChunks() |
ChunkGroup 的层级关系细节
关键理解:
Entrypoint是 ChunkGroup 的子类,代表一个入口点- 一个 Entrypoint 可以包含多个 Chunk(Runtime + Initial + Async children)
- Async ChunkGroup 形成了懒加载链路:父 ChunkGroup 加载完成后再按需加载子 ChunkGroup
- CSS Chunk 作为 Async Chunk 的辅助产物(auxiliaryFile)存在
四、四种产物类型的详细解析
Webpack 5 最终会产出四类不同性质的 Chunk 文件:
4.1 Initial Chunk(入口 Chunk)
定义:对应 entry 配置的同步加载产物,通过 HTML 中的 <script> 标签直接引入。
命名规则:由 output.filename 控制
// webpack.config.js
module.exports = {
output: {
filename: '[name].[contenthash:8].js', // 主 Chunk 命名
// 例如: main.a3b2c1d4.js
},
entry: {
main: './src/main.js',
},
};特征:
- 包含入口模块及其所有同步依赖
- 页面加载时必须立即下载(除非使用
<script defer/async>) - 可被浏览器缓存(配合 contenthash)
4.2 Async Chunk(异步 Chunk)
定义:由动态 import() 或 require.ensure() 触发的按需加载产物。
命名规则:由 output.chunkFilename 控制(注意不是 filename!)
module.exports = {
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js', // Async Chunk 命名
// 例如: src_dashboard_js.7e8f9g0h.chunk.js
},
};常见坑点:很多开发者只配了
filename而忘了chunkFilename,导致 Async Chunk 使用默认的[id].js命名,不利于长期缓存。
加载方式:通过 JSONP(JSON with Padding)机制动态插入 <script> 标签加载:
// Webpack 生成的动态加载代码(简化)
__webpack_require__.e("src_dashboard_js") // Promise
.then(__webpack_require__.bind(__webpack_require__, "src_dashboard_js"))
.then(module => { /* 使用 module */ });__webpack_require__.e 的内部逻辑:
- 检查该 Chunk 是否已加载(通过 installedChunks 缓存)
- 若未加载,创建
<script>标签,src指向 chunkFilename 生成的 URL - 通过全局变量回调(JSONP)将模块注册到
installedModules - resolve Promise
4.3 Runtime Chunk(运行时 Chunk)
定义:Webpack 注入的引导代码,负责:
- 实现
__webpack_require__模块系统 - 管理 Module 缓存 (
installedModules) - 处理 HMR(热更新)连接
- 处理异步加载 (
__webpack_require__.e) - 处理 Module Federation 远程模块解析
提取方式:
// 方式一:entry.runtime 配置(推荐用于多 entry 共享 runtime)
module.exports = {
entry: {
main: { import: './src/main.js', runtime: 'runtime' },
about: { import: './src/about.js', runtime: 'runtime' },
},
};
// 方式二:runtimeChunk 配置(更简洁)
module.exports = {
optimization: {
runtimeChunk: {
name: 'runtime', // 或 true(自动命名)
},
},
};
// 方式三:单 name 字符串(等同于 { name: 'xxx' })
module.exports = {
optimization: {
runtimeChunk: 'single', // 所有 entry 共享一个 runtime
},
};三种 runtimeChunk 值对比:
| 值 | 行为 | 适用场景 | |----|------|---------| | false / 不配置 | Runtime 内嵌在每个 Initial Chunk 中 | SPA 单页应用 | | true / 'single' | 所有 entry 共享一个 Runtime Chunk | 多 entry 应用 | | 'multiple' | 每个 entry 独立的 Runtime Chunk | 微前端 / 独立部署场景 | | { name: 'xxx' } | 自定义名称,共享同一个 Runtime | 需要精确控制名称时 |
4.4 CSS Chunk(样式 Chunk)
定义:通过 mini-css-extract-plugin(或 Webpack 5 的 experiments.css)提取的 CSS 产物。
命名规则:
const MiniCssExtractPlugin = require('mini-css-extract-plugin');
module.exports = {
plugins: [
new MiniCssExtractPlugin({
filename: '[name].[contenthash:8].css', // Initial CSS
chunkFilename: '[id].[contenthash:8].chunk.css', // Async CSS
}),
],
};Webpack 5 experiments.css:Webpack 5 实验性地支持原生 CSS 处理(无需额外 Loader),可通过
experiments.css: true启用,此时 CSS 模块会被视为一等公民 Module,最终以 CSS Chunk 形式输出。
CSS Chunk 的特殊之处:
- 它不是 Chunk 的主产物(
files),而是辅助产物(auxiliaryFiles) - 这意味着一个 JS Chunk 可以同时关联一个 CSS Chunk
- CSS Chunk 的加载时机由 JS Chunk 决定(通过
link标签注入)
五、三种产物类型的输出流程对比
下面这张图清晰展示了三类 JS 产物从 Chunk 到最终加载的完整链路差异:
六、完整示例推演:多 Entry + Code Splitting
下面我们用一个完整的例子,从头到尾推演 Chunk 的创建过程。
6.1 项目结构与配置
src/
├── main.js # 入口 A
├── about.js # 入口 B
├── shared-utils.js # 公共工具(被两个入口共同依赖)
├── components/
│ ├── Header.js # main.js 同步依赖
│ ├── Footer.js # main.js 同步依赖
│ └── Dashboard.js # main.js 异步依赖
├── pages/
│ └── Settings.js # Dashboard.js 异步依赖// main.js
import './shared-utils.js';
import Header from './components/Header.js';
import Footer from './components/Footer.js';
Header.render();
Footer.render();
document.getElementById('btn').addEventListener('click', () => {
import(/* webpackChunkName: 'dashboard' */ './components/Dashboard.js')
.then(({ default: Dashboard }) => Dashboard.render());
});// about.js
import './shared-utils.js';
console.log('About page');// components/Dashboard.js
import Chart from './Chart.js'; // 同步依赖
import(/* webpackChunkName: 'settings' */ '../pages/Settings.js'); // 异步嵌套
export default { render: () => console.log('Dashboard') };// webpack.config.js
module.exports = {
mode: 'production',
entry: {
main: { import: './src/main.js', runtime: 'runtime' },
about: { import: './src/about.js', runtime: 'runtime' },
},
output: {
filename: '[name].[contenthash:8].js',
chunkFilename: '[name].[contenthash:8].chunk.js',
clean: true,
},
optimization: {
splitChunks: {
chunks: 'all',
cacheGroups: {
vendor: {
test: /[\\/]node_modules[\\/]/,
name: 'vendors',
chunks: 'all',
},
},
},
},
};6.2 Chunk 创建推演
6.3 最终产物清单
经过完整流程后,Webpack 将输出以下文件:
| 文件名 | 类型 | 来源 | 说明 | |--------|------|------|------| | runtime.xxx.js | Runtime Chunk | 自动生成 | 运行时引导代码,main 和 about 共享 | | main.xxx.js | Initial Chunk | entry.main | 入口 main 的业务代码 | | about.xxx.js | Initial Chunk | entry.about | 入口 about 的业务代码 | | dashboard.xxx.chunk.js | Async Chunk | import(Dashboard) | 按需加载的仪表盘模块 | | settings.xxx.chunk.js | Async Chunk | import(Settings) (嵌套异步) | 设置页面(Dashboard 的子异步) | | shared-utils.xxx.chunk.js | Async Chunk | SplitChunks 提取 | 被抽取的公共模块 | | vendors.xxx.chunk.js | Async Chunk | SplitChunks 提取 | node_modules 第三方库(如有) |
6.4 ChunkGroup 依赖关系图
七、Deterministic Module/Chunk IDs —— 保证长期缓存稳定
Webpack 5 引入了 Deterministic(确定性)ID 算法,解决了长期缓存的一个核心痛点:新增/删除模块不应影响其他模块的 ID。
问题背景
在 Webpack 4 及之前,默认使用数字自增 ID(0, 1, 2, ...)。问题在于:
构建第 1 次: main(0), utils(1), dashboard(2)
构建第 2 次: main(0), utils(1), new-feature(2), dashboard(3) ← dashboard ID 变了!一旦某个 Module 的 ID 变化,依赖它的 Chunk 的 hash 也会变化,导致缓存失效范围扩大。
Deterministic ID 算法
Webpack 5 默认使用 deterministic 模式:
module.exports = {
optimization: {
moduleIds: 'deterministic', // 默认值(production 模式下)
chunkIds: 'deterministic', // 默认值(production 模式下)
},
};工作原理:
- 基于 Module/Chunk 的内容哈希和路径信息生成短 hash ID(通常 3-4 位字符)
- 相同内容的 Module 在不同构建间获得相同 ID
- 新增 Module 不会改变已有 Module 的 ID
构建第 1 次: main(a1b), utils(c2d), dashboard(e3f)
构建第 2 次: main(a1b), utils(c2d), new-feature(g4h), dashboard(e3f) ← dashboard ID 不变 ✓ID 策略选项
| 值 | 产物大小 | 长期缓存 | 适用场景 | |----|---------|---------|---------| | 'natural' | 最小 | ❌ 差 | 开发模式 | | 'named' | 小 | ✅ 中 | 开发调试 | | 'deterministic' | 小 | ✅✅ 最佳 | 生产环境推荐 | | 'size' | 最小 | ✅ 中 | 极致体积优化 |
八、output 配置对 Chunk 命名的完整影响
8.1 filename vs chunkFilename
module.exports = {
output: {
path: path.resolve(__dirname, 'dist'),
// 控制 Initial Chunk + Runtime Chunk 的命名
filename: (pathData) => {
const isRuntime = pathData.chunk.name === 'runtime';
return isRuntime
? 'runtime.[fullhash:8].js'
: '[name].[contenthash:8].js';
},
// 控制 Async Chunk 的命名
chunkFilename: '[name].[contenthash:8].lazy.js',
// Webpack 5: CSS 文件命名(experiments.css 启用时)
// cssFilename: '[name].[contenthash:8].css',
// cssChunkFilename: '[id].[contenthash:8].css',
// 自动推断 publicPath(Webpack 5 特性)
// auto: true,
},
};8.2 可用的占位符
| 占位符 | 说明 | 示例 | |--------|------|------| | [name] | Chunk 名称 | main, dashboard | | [id] | Chunk ID(deterministic hash) | a1b | | [contenthash:8] | Chunk 内容哈希(前 8 位) | e3f7a2b1 | | [fullhash:8] | 整次构建的完整哈希 | c4d8f1a2 | | [ext] | 文件扩展名 | js | | [query] | 查询参数 | (URL 参数) |
九、默认分包规则的问题与演进
9.1 核心问题:模块重复
默认分包规则最大的问题是无法解决跨 Chunk 的模块重复:
Chunk[main] ──→ shared-utils.js ✓
Chunk[about] ──→ shared-utils.js ✓ ← 重复打包!在没有 splitChunks 的情况下,Webpack 只是将 Module 机械地分配到各个 Chunk,不做去重处理。
9.2 历史演进
| 版本 | 方案 | 问题 | |------|------|------| | Webpack 1-2 | 无解决方案 | 模块完全重复 | | Webpack 3 | CommonsChunkPlugin | 基于简单父子链,容易误判父子关系,反而可能恶化性能 | | Webpack 4+ | SplitChunksPlugin + ChunkGroup | 启发式算法,基于图结构智能分析,支持多维度策略 | | Webpack 5 | 优化 SplitChunks + deterministic IDs | 更稳定的长期缓存 + 更细粒度的控制 |
9.3 为什么 CommonsChunkPlugin 失败了?
CommonsChunkPlugin 的本质缺陷在于它基于 Chunk 之间简单的线性父子关系 来提取公共模块。但实际的依赖关系是一个有向无环图(DAG),而非线性链条:
实际情况(DAG):
Chunk[main]
│
├──→ Chunk[vendor]
│
└──→ Chunk[common]
Chunk[about]
│
├──→ Chunk[vendor]
│
└──→ Chunk[common]
CommonsChunkPlugin 错误地假设为线性关系:
Chunk[common] → Chunk[vendor] → Chunk[main]
→ Chunk[about]这种错误假设导致它无法正确判断提取出的公共 Chunk 应该作为父 Chunk 还是子 Chunk,某些场景下反而增加了请求次数。
Webpack 4 引入的 ChunkGroup 数据结构彻底解决了这个问题——它能够表达复杂的多对多依赖关系,配合 SplitChunksPlugin 的图论算法实现真正的智能分包。
十、总结
让我们用一张全景图回顾 Chunk 的完整世界:
核心要点回顾:
- Chunk 是产物的组织单位:一个 Chunk 是一组 Module 的集合,最终输出为一个或多个文件
- seal 阶段的五个关键步骤:Entry 创建 → Module 关联 → ChunkGraph 构建 → 优化 → 产物输出
- ChunkGroup 表达了 Chunk 间的加载顺序和依赖关系:Entrypoint → Initial/Async/Runtime Chunks
- 四种产物类型各有不同的加载方式和命名规则:Initial(同步)、Async(按需)、Runtime(引导)、CSS(辅助)
- SplitChunksPlugin 在优化阶段介入,是对内置分包规则的二次重组,而非替代
- Deterministic IDs 保证长期缓存稳定性,是生产环境的最佳实践
「封装」阶段最重要的目标始终是:确定有多少个 Chunk,以及每一个 Chunk 中包含哪些 Module —— 这些才是真正影响最终打包结果的关键因素。其它一切优化(压缩、Tree Shaking、SplitChunks)都是围绕这个目标服务的手段而已。
思考题
-
Chunk 一定会且只会产生一个产物文件吗?为什么?
mini-css-extract-plugin、file-loader这一类能写出额外文件的插件,底层是怎么实现的?(提示:关注chunk.auxiliaryFiles) -
如果一个 Module 同时被一个 Initial Chunk 和一个 Async Chunk 引用,在默认分包规则和开启
splitChunks.chunks: 'all'时,这个 Module 分别会如何处理? -
optimization.runtimeChunk: 'multiple'和runtimeChunk: 'single'在微前端场景下各有什么优劣?如何选择? -
Deterministic ID 的长度(默认 3-4 位)是否可能发生碰撞?Webpack 如何处理潜在的 ID 冲突?
参考源码路径(Webpack 5.107):
lib/Compilation.js—seal()方法lib/buildChunkGraph.js— ChunkGraph 构建核心lib/Entrypoint.js— Entrypoint 类lib/ChunkGroup.js— ChunkGroup 类lib/Chunk.js— Chunk 类lib/util/deterministicModuleIdPlugin.js— Deterministic ID 算法