Webpack 多页应用混合路由(Hybrid Routing)示例深度解析:多入口、共享路由 bundle 与页面级按需加载 Webpack 多页应用混合路由Hybrid Routing示例深度解析多入口、共享路由 bundle 与页面级按需加载【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack多页应用MPA常常需要首屏整页直出、页面切换时再加载目标页代码的混合路由体验每个页面是一个独立 HTML 入口而路由与公共逻辑可以跨页共享非当前页的业务代码则按需异步加载。webpack 官方在 examples/hybrid-routing 目录中提供了一个最小可运行的示例通过多入口 入口数组共享路由模块 import()表达式按页分包三种手段的组合演示如何把每页一个完整 bundle和路由切换才加载对应页面的两种诉求融合进同一套构建配置。读完本文你将理解该示例中每个源文件与每行配置的用途能读懂其 dist 产物中的异步上下文lazy context与 JSONP 运行时并可照此模式在自己的 MPA 项目中落地共享公共代码 页面级代码分割的方案。示例总览一个两页的混合路由站点先看 示例目录 的文件清单它非常精简恰好够表达混合路由的所有关键元素webpack.config.js多入口 公共代码抽取配置aEntry.js 与 bEntry.jsA、B 两个页面各自的入口文件aPage.js 与 bPage.jsA、B 两个页面各自的业务模块router.js被两个入口共享的路由逻辑负责按需import()页面模块render.js公共渲染函数把页面模块执行结果输出到控制台pageA.html 与 pageB.html两个页面各自的 HTML 宿主示范如何手工静态引用构建产物。整个依赖关系可以归纳为一张简单的图pageA.bundle.js入口 chunkaEntry.js router.js render.js 运行时 ├── aPage.bundle.js异步 chunkaPage.js pageB.bundle.js入口 chunkbEntry.js router.js render.js 运行时 └── bPage.bundle.js异步 chunkbPage.js关键在两点每个入口都把自己的业务入口和 router.js 写进同一个 entry 数组让路由代码天然存在于每个页面的入口 chunk 中页面模块不随入口同步打包而是由 router.js 通过动态import()在切换时异步拉取。这正是混合路由的含义——整页与局部异步并存。核心代码拆解从入口到按需加载页面入口与页面模块页面 A 的入口 aEntry.js 本身毫无技巧就是 CommonJS 风格的普通启动代码// Just show the page a var render require(./render); render(require(./aPage));即取到页面模块./aPage交给公共的render函数渲染。示例注释同时提示bEntry.js结构与此完全类似对照源码可见 bEntry.js 只是把模块名换成./bPage。当页面很多时这种结构高度模板化所以注释补充了一句实践经验——你可能想用一个 loader 来生成这种文件即让构建层自动为每页产出 entry而不是手写 N 份重复代码。页面模块 aPage.js 同样极简module.exports function() { return This is page A.; };bPage.js 与之对等只是返回字符串不同。公共渲染函数 render.js 接收页面模块后执行并打印module.exports function(page) { console.log(page()); };在这个玩具例子里aEntry/bEntry 已经通过静态require(./aPage)引用了页面看起来按需加载似乎被破坏了——但注意入口静态 require 的是同步渲染当前页而路由负责的切换到别的页面走的是另一条动态路径。dist 产物中aPage/bPage之所以仍然被拆成独立异步 chunk是因为 router.js 里的import()引用关系让 webpack 知道这两个模块同时被动态使用于是把它们放进可以被延迟加载的 chunk。路由模块用import()表达式做动态 require真正实现切换页面才加载的是 router.jsvar render require(./render); // Event when another page should be opened // Maybe hook click on links, hashchange or popstate window.onLinkToPage function onLinkToPage(name) { // name is a or b // require the page with a dynamic require // Its important that this require only matches the pages // otherwise there is blood in the bundle. Here this is done with a // specific file prefix. Its also possible to use a directory, // overwriting the RegExp with the ContextReplacementPlugin, or // using the require.context method. // This line may throw a exception on runtime if the page wasnt found. import(/* webpackChunkName: [request] */./${name}Page).then(page {; render(page.default); }); }这段代码至少演示了四个要点路由入口约定示例以window.onLinkToPage(name)作为切换页面的对外接口注释说明实际项目中通常把它挂在链接的 click、hashchange或popstate事件上。name 取a或b。动态 import 的目标是模板字符串import(\./${name}Page) 不是一个静态路径。webpack 无法在编译期确定具体文件只能把它当作一个表达式处理自动收集所有能匹配的模块构成一个上下文context。匹配范围必须收窄注释里有一句口语化但非常重要的提醒——Its important that this require only matches the pages, otherwise there is blood in the bundle意思是如果表达式写得太宽例如./${name}webpack 会把目录下大量无关模块全部纳入上下文候选导致 bundle 被无关代码污染。本示例用Page后缀来精确限定只有./aPage、./bPage这类文件被匹配注释给出了其他三种收敛手段使用独立目录、用ContextReplacementPlugin改写匹配正则、或用require.context方法手动构建上下文。magic comment 控制 chunk 名/* webpackChunkName: [request] */中的[request]会在编译期被替换为被匹配模块的请求字符串从而把 aPage/bPage 各自命名的异步 chunk 命名为aPage/bPage让产物文件名可读、可预测。失败语义注释特别指出若运行时传入的 name 匹配不到任何模块这一行会抛异常dist 产物里可以看到对应的Cannot find module xxx错误路径因此生产代码需要为路由目标不存在准备容错处理。另外page.default的出现值得说明对 ES Module 使用import()时.then回调拿到的是模块命名空间对象默认导出挂在default上。这里 aPage/bPage 是 CommonJS 写法但 webpack 生成的异步上下文同样会统一包装成命名空间详见下文 dist 解读中的__webpack_require__.t所以page.default依然取得到module.exports。多入口配置让路由成为每个页面的既有模块核心配置见 webpack.config.js第 8–31 行逐段解读如下use strict; const path require(path); /** type {import(webpack).Configuration} */ const config { // mode: development || production, entry: { // The entry points for the pages // They also contains router pageA: [./aEntry, ./router], pageB: [./bEntry, ./router] }, output: { path: path.join(__dirname, dist), publicPath: js/, filename: [name].bundle.js, chunkFilename: [name].chunk.js }, optimization: { // Extract common modules from initial chunks too // This is optional, but good for performance. splitChunks: { chunks: all, minSize: 0 // This example is too small }, chunkIds: named // To keep filename consistent between different modes (for example building only) } }; module.exports config;各选项的意图与效果配置项值含义与在本示例中的作用entry.pageA/entry.pageB字符串数组[./aEntry, ./router]数组中的模块会合并进同一个入口 chunk且数组内模块之间存在依赖时按依赖顺序执行。于是 router.js 无需被每个入口手动require也会同时进入 pageA、pageB 两个入口 chunk成为每页都携带的路由代码。output.filename[name].bundle.js入口 bundle 的文件名模板[name]取 entry key产物即pageA.bundle.js、pageB.bundle.js。output.chunkFilename[name].chunk.js异步 chunk 的文件名模板。output.pathpath.join(__dirname, dist)产物输出到示例目录下的dist/。output.publicPathjs/运行时动态加载 chunk 时使用的 URL 前缀见下文HTML 挂载与 publicPath一节。optimization.splitChunks.chunks: all对所有 chunk含异步参与公共代码抽取注释强调Extract common modules from initial chunks too——默认行为只抽取异步 chunk 间的公共模块all让入口 chunk 之间的公共模块也被抽出来共享避免 router.js 的代码在每个入口 bundle 里各存一份。optimization.splitChunks.minSize: 00覆盖默认的最小字节阈值。示例代码量太小不设 0 的话公共模块会因体积不达标而不被拆分。optimization.chunkIds: namednamed让 chunk 使用可读名称而非数字 id。注释说明这是为了在不同 mode 下例如只做某一次构建对比时产物文件名保持一致。需要强调splitChunks.chunks: all加上入口数组的连锁效应router.js 同时是 pageA、pageB 的入口模块属于两个初始 chunk 共有的模块因此会被抽进一个公共 chunk而chunks: all又让被import()引用的 aPage/bPage 也可以进入公共缓存组的候选范围。从 dist 统计信息看最终形成pageA.bundle.js、pageB.bundle.js两个入口 chunk、一个含 router.js 与 render.js 的router_js.bundle.js公共 chunk以及aPage.bundle.js、bPage.bundle.js两个异步 chunk详见下文产物解读。dist 产物解读异步上下文与 JSONP 运行时如何工作示例 README 内嵌了构建产物作为教学材料其中最有价值的是两个文件dist/router_js.bundle.jsREADME 内展示的router_js.bundle.js和 dist/pageA.bundle.js。它们不是手工代码而是 webpack 在编译上面源码时真实生成的运行时。读懂它们就能理解import()表达式与异步 chunk 的底层实现。说明README 中的dist/*内容由仓库的示例构建流程生成后嵌入其模板机制见 template.md 与 template-common.js下述代码为其中关键片段的整理节选。异步上下文模块一张请求 → chunk的映射表router.js 的import(\./${name}Page)会被编译成对**异步上下文模块webpackAsyncContext**的调用。router_js.bundle.js 中最核心的部分如下const map { ./aPage: [ 2, [aPage] ], ./bPage: [ 6, [bPage] ] }; function webpackAsyncContext(req) { try { if(!__webpack_require__.o(map, req)) { return Promise.resolve().then(() { const e new Error(Cannot find module req ); e.code MODULE_NOT_FOUND; throw e; }); } } catch(err) { return Promise.reject(err); } const ids map[req], id ids[0]; return __webpack_require__.e(ids[1][0]).then(() (__webpack_require__.t(id, 7 | 16))); } webpackAsyncContext.keys () (Object.keys(map)); webpackAsyncContext.id 4; module.exports webpackAsyncContext;这一段把上下文的实现讲得很直白map记录每个可匹配请求./aPage、./bPage对应的模块 id 与所属 chunk 名查询请求不在 map 中时返回一个被 reject 的 Promise 并抛MODULE_NOT_FOUND——这就是源码注释所说运行时可能抛异常的具体形态命中时先__webpack_require__.e(aPage)确保对应 chunk 已加载再用__webpack_require__.t(id, 7 | 16)把模块包装成命名空间对象返回因此page.default可用该上下文模块的匹配正则范围从产物中可以反推为^\.\/.*Page$stats 信息里明确标注了lazy ^\.\/.*Page$。这段代码生成逻辑位于 webpack 的 lib/ContextModule.js其中约第 992–1026 行即为webpackAsyncContext的运行时代码模板是动态 import 表达式 上下文收敛这一整套机制的实际落地处。入口 bundle 运行时module cache、chunk 加载与启动编排pageA.bundle.js是标准的 webpack bootstrap 产物结构上分三块模块表这里只有./aEntry.js一个普通模块、运行时模块、以及启动逻辑。入口模块表如下var __webpack_modules__ ([ /* 0 */ /*! ./aEntry.js !*/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) { // Just show the page a var render __webpack_require__(/*! ./render */ 1); render(__webpack_require__(/*! ./aPage */ 2)); }) ]);运行时部分由若干 runtime module 组成作用一目了然模块缓存与__webpack_require__经典 CJS 风格运行时——先查__webpack_module_cache__未命中则创建{ exports: {} }并执行模块函数__webpack_require__.Ochunk loaded管理入口依赖的异步 chunk 尚未加载完时挂起的启动回调是延迟执行的调度器__webpack_require__.tcreate fake namespace objectmode 16表示值若是 Promise-like 直接返回mode 7组合出先 require 再包装成命名空间的行为——这正是异步上下文返回page.default的支撑__webpack_require__.e__webpack_require__.f.jensure chunk / jsonp chunk loadingJSONP 加载异步 chunk先查询installedChunks未加载则创建一个 Promise 并通过__webpack_require__.l注入script标签请求publicPath chunkId 扩展名__webpack_require__.lload script管理脚本的 onload/onerror、120 秒超时、内存泄漏防护IE 场景与nonce配合 CSP__webpack_require__.ppublicPath示例构建产物中该值为dist/用于拼接异步 chunk 的真实 URLwebpackJsonpCallback与chunkLoadingGlobal全局self[webpackChunk]数组的push被替换为回调函数异步 chunk 文件用(self[webpackChunk] self[webpackChunk] || []).push(...)把模块表登记进运行时的模块表并 resolve 对应加载 Promise。异步 chunk如 dist/aPage.bundle.js 展示的aPage.bundle.js的内容就是一行 JSONP push把./aPage.js的模块函数与 chunk 名[aPage]注册进来(self[webpackChunk] self[webpackChunk] || []).push([[aPage],{ /***/ 2 /*! ./aPage.js !*/ (module) { module.exports function() { return This is page A.; }; } }]);最后是启动编排——由于 pageA 的入口同时依赖router_js与aPage两个 chunkbootstrap 结尾用__webpack_require__.O挂起执行等这些 chunk 就绪后再真正跑入口模块// startup // Load entry module and return exports // This entry module depends on other loaded chunks and execution need to be delayed __webpack_require__.O(undefined, [router_js,aPage], () (__webpack_require__(0))) let __webpack_exports__ __webpack_require__.O(undefined, [router_js,aPage], () (__webpack_require__(3))) __webpack_exports__ __webpack_require__.O(__webpack_exports__);对比一下 router.js 的源码模块 id在router_js.bundle.js中render.js 是模块 1、router.js 是模块 3、异步上下文是模块 4而 pageA 启动代码里__webpack_require__(0)对应 aEntry、__webpack_require__(3)对应 router.js——跨 chunk 的模块通过公共运行时共享同一张模块表这正是router.js 被抽进公共 chunk 但仍能与入口联动的微观体现。页面 HTML 挂载与 publicPath 的配套关系两个宿主页面之一 pageA.html 的内容为html head/head body script async srcdist/pageA~pageB.chunk.js charsetutf-8/script script async srcdist/aPage.chunk.js charsetutf-8/script script async srcdist/pageA.bundle.js charsetutf-8/script /body /htmlpageB.html 结构相同只是把后两个 script 换成dist/bPage.chunk.js与dist/pageB.bundle.js。示例借此说明一个多页应用 HTML 页面通常要静态引用的三类产物公共 chunk、页面自己的异步 chunk若首屏就要用、以及入口 bundle。需要提醒的是这类手工维护的 script 引用必须与真实产物名保持同步例如本示例 HTML 与当前配置下实际产物名存在出入且output.publicPath配置为js/而 HTML 里的静态引用写作dist/前缀实践中更稳妥的做法是把 publicPath、chunk 命名与产物放置位置统一规划或交给能自动注入产物引用的插件来生成 HTML避免手工写死文件名导致加载 404。同时注意这些 HTML 使用async加载入口之前的公共 chunk以保证执行顺序正确——async脚本不能保证加载顺序因此依赖关系正确时一般把公共 chunk 放在前面、或使用非 async 的同步引用。构建统计解读两种 mode 下的产物格局README 的 Info 部分给出了同一次源码在未优化/开发与 Production 模式下的完整构建统计两者产出的 chunk 划分完全一致只是体积不同。整理如下。Unoptimized默认 mode产物清单资产体积说明pageA.bundle.js/pageB.bundle.js各 12.5 KiB入口 bundle含约 7.39 KiB 运行时router_js.bundle.js2.59 KiB公共 chunkrouter.js render.jsaPage.bundle.js/bPage.bundle.js各 380 bytes异步页面 chunk两个入口的构成完全对称Entrypoint pageA 15.5 KiB router_js.bundle.js 2.59 KiB aPage.bundle.js 380 bytes pageA.bundle.js 12.5 KiBpageB 同理。chunk 关系中的几个标注值得解释aPage.bundle.js同时被import()context 元素与cjs require来自 aEntry.js 3:7-25引用被标记为[initial] [rendered] reused as split chunk (cache group: default)——意思是它虽然被入口静态引用但因为同时是异步上下文的候选被划归默认缓存组并作为独立 split chunk 复用router_js.bundle.js标注split chunk (cache group: default)内部是 router.js 与 render.jsdependent modules并被两个入口共同引用验证了splitChunks从初始 chunk 中抽取公共代码的效果每个 chunk 都列出了完整的构建来源 ./xxx entry ...与import() context element可以用来反向追踪模块归因。Production 模式产物清单资产体积说明pageA.bundle.js/pageB.bundle.js各 2.83 KiB已压缩minimizedrouter_js.bundle.js582 bytes已压缩aPage.bundle.js/bPage.bundle.js各 116 bytes已压缩入口体积从 15.5 KiB 降到约 3.52 KiB压缩收益明显chunk 划分、模块归因与开发模式完全一致这正是配置里chunkIds: named保证不同 mode 下文件名一致的意义所在——同一份拆分逻辑下产物名不因模式切换而漂移便于对比与缓存规划。可复现与扩展在仓库中运行该示例该示例属于 webpack 仓库examples/目录下的官方教学用例目录入口见 examples/README.md 的 Hybrid Routing 小节。本地复现构建的方式很直接在 examples/hybrid-routing 目录下以仓库内的 webpack 作为依赖执行构建即可例如在仓库根目录完成依赖安装后执行cd examples/hybrid-routing node ../../bin/webpack.js --config webpack.config.js # 默认构建 node ../../bin/webpack.js --config webpack.config.js --mode production # 压缩产物注意两点前提一是示例本身不提供node_modules需要先在仓库根目录安装依赖使示例能解析到webpack包二是示例产物默认输出到dist/重复构建前可先清理该目录避免混淆。仓库还提供了批量构建所有示例的脚本 examples/buildAll.js它会读取 examples/examples.js 的示例清单逐个执行构建README 中的源码与统计信息并非手工抄写而是由 template.md 配合 template-common.js 的占位符替换机制生成模板中的_{{...}}_变量会被实际文件内容与构建 stdout 替换如_{{webpack.config.js}}_、_{{production:stdout}}_。落到真实项目上这个模式可以按三个方向自然演进入口模板化入口文件aEntry/bEntry结构重复正如示例注释所建议的可以用自定义 loader 按页面清单批量生成或直接通过entry的配置化数据驱动收敛上下文当页面数量增多、需要从多层目录按约定加载时可用独立目录 require.context或在必要时用ContextReplacementPlugin改写匹配正则控制异步上下文的候选范围避免无关模块混入对应 router.js 注释给出的三种备选方案路由事件源示例以window.onLinkToPage占位实际可替换为hashchange/popstate监听或任意前端路由库的跳转回调让事件 → 动态import()→ chunk 加载 → 渲染页面成为一套通用的 MPA 切换链路。如果需要继续对照仓库中的同类机制require.context、code-splitting-specify-chunk-name讲解webpackChunkName与[request]命名、code-splitting 与 common-chunk-and-vendor-chunk 等官方示例分别从上下文、命名分包与公共 chunk 抽取等角度提供了互补的视角。【免费下载链接】webpackA bundler for javascript and friends. Packs many modules into a few bundled assets. Code Splitting allows for loading parts of the application on demand. Through loaders, modules can be CommonJs, AMD, ES6 modules, CSS, Images, JSON, Coffeescript, LESS, ... and your custom stuff.项目地址: https://gitcode.com/GitHub_Trending/web/webpack创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考