Webpack 在异步请求 JS 文件时获取 JS Bundle 的机制
Webpack 在处理代码分割(Code Splitting)后产生的异步 chunk(通常是import()动态导入)时,浏览器最终是怎么知道要去请求哪个 .js 文件,以及**请求回来后怎么正确执行**,这个过程主要依赖以下几个核心机制:
核心机制概览(2024~2025 主流方式)
| 机制 | 主要文件 | 作用 | 谁负责生成 | 当前主流方式 |
|---|---|---|---|---|
| manifest | runtime chunk | chunkId → 文件名映射表 | webpack(内置) | 绝大多数项目都有 |
| publicPath | runtime 或配置 | 决定请求的 URL 前缀 | 用户配置 + runtime 推断 | 非常重要 |
| webpack_require.p | runtime | 就是 publicPath 的运行时变量 | runtime 注入 | 核心变量 |
| webpack_require.u | runtime | chunkId → chunk 文件名 的函数 | webpack(新版本更智能) | 现代主流 |
| webpack_require.e | runtime | 真正发起 chunk 加载的函数 | webpack | 异步加载入口 |
| JSONP / script tag | 浏览器 | 实际发起 .js 请求的方式 | 浏览器 | 仍然是默认(2025年) |
详细流程(以最常见的 JSONP + webpackChunkName 方式为例)
1. 代码里写: import(/* webpackChunkName: "user-detail" */ './user-detail.js') 2. 打包后生成的文件大致如下: - main.js ← 入口文件 + runtime - 123.user-detail.js ← 异步 chunk(chunkId=123) - 456.other-page.js ← 另一个异步 chunk 3. webpack 在 main.js(或单独的 runtime chunk)中注入了一段类似这样的代码: // 简化的伪代码 var installedChunks = { 0: 0 }; // 已加载的 chunk 标记 __webpack_require__.e = function requireEnsure(chunkId) { var promises = []; // 检查是否已经加载过 if (!installedChunks[chunkId]) { var promise = new Promise(function(resolve, reject) { // 记录 promise,后面 onload 会 resolve var callbacks = installedChunks[chunkId] = [resolve, reject]; // 重要!决定文件名的地方 ↓↓↓ var filename = __webpack_require__.u(chunkId); // ← 得到 "123.user-detail.js" var fullUrl = __webpack_require__.p + filename; // ← publicPath + 文件名 // 创建 script 标签 var script = document.createElement('script'); script.charset = 'utf-8'; script.timeout = 120; script.src = fullUrl; // 错误处理 script.onerror = script.onload = function(event) { // ... 处理成功/失败,把 promise resolve/reject }; document.head.appendChild(script); }); promises.push(promise); } return Promise.all(promises); } 4. 当代码执行到 import() 时,实际上调用的是: __webpack_require__.e("123").then(function() { // chunk 已经加载完成,可以使用模块了 var module = __webpack_require__("./src/user-detail.js"); // ... })几个关键问题解答
| 问题 | 答案来源 | 说明 |
|---|---|---|
| 文件名是怎么知道的? | __webpack_require__.u(chunkId) | webpack 打包时把 chunkId → 文件名映射写死或生成函数 |
| 请求路径前缀从哪来? | __webpack_require__.p(publicPath) | 通常来自 output.publicPath 配置 |
| publicPath 是相对路径怎么办? | runtime 会尝试推断(script.src 位置) | 现代 webpack 5 有比较智能的推断逻辑 |
| CDN + 版本号怎么办? | output.publicPath = ‘https://cdn.com/v1.2.3/’ | 直接写死或通过环境变量注入 |
| 如何知道 chunk 加载成功了? | script.onload + JSONP 回调 | chunk 内部会调用 webpackJsonp.push |
| 多个 chunk 同时加载会不会冲突? | webpackJsonp 是全局数组,push 的时候带 chunkId | 基本不会冲突 |
| 开发环境和生产环境的区别? | 开发环境通常用 webpack-dev-server 的内存文件系统 + sockjs | 生产环境才是真正的 .js 文件请求 |
2024-2025 年现代趋势对比表
| 方式 | chunk 名控制方式 | publicPath 处理 | 推荐场景 | 备注 |
|---|---|---|---|---|
| JSONP(默认) | webpackChunkName / id | 自动推断 + 配置 | 绝大多数项目 | 兼容性最好 |
| importScripts | 基本不用 | — | Service Worker | 特殊场景 |
| SystemJS/Federation | 远程模块名 | 由 host 决定 | Module Federation | 微前端 |
| ESM + import() | 浏览器原生 | type=“module” | 实验性、全 ESM 项目 | 未来方向,但目前还需 polyfill |
总结一句话
Webpack 异步 chunk 的加载机制本质上是:
通过运行时注入的__webpack_require__.e函数 + chunkId → 文件名映射 + publicPath 前缀,动态创建<script>标签去请求对应的 js 文件,文件执行后通过全局 webpackJsonp 回调通知 webpack 该 chunk 已就绪。
如果你想深入了解某个特殊场景(CDN 部署、Module Federation、publicPath 动态计算、chunk loading 错误重试、预加载 prefetch/preload 等),可以告诉我,我可以继续展开说明。