用内置 ManifestPlugin 生成资源清单webpack 示例工程实战解析【免费下载链接】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本指南以 examples/manifest-plugin 示例为主体讲解 webpack 内置的ManifestPlugin对应仓库源码 lib/ManifestPlugin.js它如何在构建结束后把“chunk/资源在服务器上的最终 URL”与“产生它的源码模块”之间的映射关系导出为manifest.json之类的清单文件从而让后端模板、HTML 注入器或部署脚本在运行时拿到带内容哈希的真实文件名。阅读完本文你将掌握 ManifestPlugin 的 6 个配置选项filename、prefix、entrypoints、filter、generate、serialize的用法、manifest 输出结构并能自己写一段“按入口名收集全部首屏 JS/CSS”的消费端代码。示例工程做了什么该示例位于仓库 examples/manifest-plugin 目录其构建入口 example.js 同时覆盖了三种常见资源形态import fooURL from ./foo.txt; const barURL new URL(./bar.txt, import.meta.url); async function loadAsync() { return import(./async.js); } await loadAsync(); export default [fooURL, barURL];./foo.txt经file-loader处理成带哈希文件名的独立资源同时会被记录在辅助资源auxiliaryFiles中./bar.txt通过new URL(./bar.txt, import.meta.url)由 webpack 的 URL 依赖机制抽取为静态资源./async.js通过import()动态导入构成异步 chunkasync_js内容见 async.js仅export default valueexport default [fooURL, barURL]同时演示了入口模块对外导出的用法。也就是说一次构建产物里既有同步入口 chunk、异步 chunk又有独立的文本资源与 source map。这正是验证“manifest 是否把所有关键文件都映射清楚”的理想场景。说明template.md 是示例的源文档模板其中以_{{xxx}}_占位符嵌入真实源码与构建输出由仓库的 examples/buildAll.js 配合 examples/template-common.js 渲染生成最终可读的 README.md。因此你看到的两份文档结构完全一致后者是渲染后的成品。完整配置逐项拆解示例的 webpack.config.js 完整内容如下use strict; // ts-expect-error no types for yamljs const YAML require(yamljs); const webpack require(../../); /** type {import(webpack).Configuration} */ const config { devtool: source-map, output: { chunkFilename: [name].[contenthash].js }, optimization: { chunkIds: named // To keep filename consistent between different modes (for example building only) }, module: { rules: [ { test: /foo.txt/, use: require.resolve(file-loader) } ] }, plugins: [ new webpack.ManifestPlugin({ filename: manifest.json }), new webpack.ManifestPlugin({ filename: manifest.yml, prefix: /nested/[publicpath], filter(item) { if (/.map$/.test(item.file)) { return false; } return true; }, generate(manifest) { delete manifest.assets[manifest.json]; manifest.custom value; return manifest; }, serialize(manifest) { return YAML.stringify(manifest, 4); } }) ] }; module.exports config;配置里除 ManifestPlugin 外的几个关键点直接决定了 manifest 的最终形态devtool: source-map让每个 JS 产物额外生成.map文件以便演示filter如何把这类辅助文件从清单中剔除output.chunkFilename: [name].[contenthash].js异步 chunk 使用“命名 内容哈希”的稳定文件名如async_js.0eeb6882e0cf674fd1fc.jsoptimization.chunkIds: named让异步 chunk 在普通与 production 两种模式下都使用async_js这样的可读命名保证不同模式间文件名一致配置内注释也说明了这一点否则默认会退化为数字 id 之类的模式相关命名。两个插件实例JSON 与 YAML 双输出同一个配置里注册了两个 ManifestPlugin 实例它们分别演示了“默认行为”与“高度定制”两种用法配置项第一个实例manifest.json第二个实例manifest.ymlfilenamemanifest.json可省略见下方默认值manifest.ymlprefix不填默认[publicpath]/nested/[publicpath]构建后自动展开为/nested/dist/之类的前缀filter不填保留全部资源用正则/\.map$/丢掉所有 source mapgenerate不填原样输出删除 manifest 自引用条目assets[manifest.json]并追加自定义字段custom: valueserialize不填默认JSON.stringify(manifest, null, 2)用yamljs的YAML.stringify(manifest, 4)输出缩进 4 格的 YAML选项定义与校验源码视角ManifestPlugin 的所有选项在编译期都会经过 schema 校验。插件的apply阶段通过compiler.hooks.validate注册了校验逻辑见 lib/ManifestPlugin.js校验规则定义在 schemas/plugins/ManifestPlugin.json。从 schema 可以看出官方对每个选项的精确约束filename输出文件在磁盘上的文件名类型 string、最小长度 1、不允许绝对路径注释明确“默认在output.path目录内输出manifest.json”对应源码常量DEFAULT_FILENAME manifest.jsonlib/ManifestPlugin.jsprefix为 manifest 中每个file路径增加的前缀字符串schema 仅要求 stringentrypoints布尔值控制是否生成entrypoints段默认为true源码 lib/ManifestPlugin.js 中this.options.entrypoints ! undefined ? this.options.entrypoints : truefilter接收每个 manifest 条目、返回布尔值的函数(item: ManifestItem) boolean返回false表示不收录该资源generate接收整个 manifest 对象、修改后返回新对象的函数(manifest) ManifestObject典型用途就是删除自引用条目、注入自定义字段serialize把 manifest 对象序列化成最终字符串的函数(manifest) string默认是美化 JSON。对应 TypeScript 类型定义可在 declarations/plugins/ManifestPlugin.d.ts 查看其 Filter / Generate / Serialize 的类型注解分别引用lib/ManifestPlugin中导出的同名 typedef。读懂两份输出manifest.json 与 manifest.ymlmanifest.json默认行为{ entrypoints: { main: { imports: [ main.js ] } }, assets: { foo.txt: { file: dist/3ee037f347c64cc372ad18857b0db91f.txt, src: foo.txt }, bar.txt: { file: dist/a0145fafc7fab801e574.txt, src: bar.txt }, output.js.map: { file: dist/output.js.map }, main.js: { file: dist/output.js }, async_js.js.map: { file: dist/async_js.0eeb6882e0cf674fd1fc.js.map }, async_js.js: { file: dist/async_js.0eeb6882e0cf674fd1fc.js } } }manifest.yml高度定制后的效果entrypoints: main: imports: - main.js assets: foo.txt: file: /nested/dist/3ee037f347c64cc372ad18857b0db91f.txt src: foo.txt bar.txt: file: /nested/dist/a0145fafc7fab801e574.txt src: bar.txt main.js: file: /nested/dist/output.js async_js.js: file: /nested/dist/async_js.0eeb6882e0cf674fd1fc.js custom: value两份清单的差异正好与上面配置一一对应可以逐点验证插件的处理逻辑file前缀被改写manifest.json 中file形如dist/…默认[publicpath]占位符被替换为构建时解析出的 publicPath 后得到的前缀manifest.yml 则在每个file前多了/nested/前缀这是因为配置了prefix: /nested/[publicpath]占位符替换后拼出完整 URL 路径。源码中前缀替换逻辑见 lib/ManifestPlugin.jsprefix默认值是DEFAULT_PREFIX [publicpath][publicpath]不区分大小写会被替换为解析后的output.publicPath当其为auto时退化为/。这解释了为什么“同一份 manifest 换一个 CDN 前缀部署”只需要改动prefix。.map文件被filter排除manifest.json 中保留了output.js.map与async_js.js.map两个 source map 条目而 manifest.yml 中两者都消失——正是filter中/\.map$/正则的作用。generate删除自引用并注入自定义字段manifest.yml 中不存在manifest.json自身的条目因为generate里执行了delete manifest.assets[manifest.json]同时多出的custom: value字段正是manifest.custom value的产物。serialize决定输出格式同一份 manifest 对象manifest.json 走默认美化 JSONmanifest.yml 被YAML.stringify(manifest, 4)序列化为 YAML且两级 key如assets下的文件条目按字母顺序排列。字段语义key 与 value 的对应关系从 schemas/plugins/ManifestPlugin.json 的 definitions 可以还原清单中每条记录的确切含义assets.key资源映射表。key 是“逻辑名”value 是一个ManifestItemfile资源在服务器上的最终 URL/输出路径必填src产生该资源前的源码模块路径相对 context仅在 webpack 能拿到asset.info.sourceFilename时才存在。这解释了为什么foo.txt、bar.txt有src字段源自同名源码文件而output.js、*.map等由 chunk 直接产出的文件没有src。entrypoints.name入口点信息imports是一个字符串数组元素是上文assets里的逻辑 key如main.js消费方通过它索引真实文件名当入口点存在父入口entrypoint.getParents()非空时还会追加parents数组schema 中parents为可选。为什么 assets 的 key 会出现main.js、async_js.js这样的“逻辑名 扩展名”看源码实现ManifestPlugin 会为每个 chunk 的每个文件调用handleFile(file, usedName)lib/ManifestPlugin.js其中usedName chunkName . extname(file)。这里扩展名的计算是经过特别处理的源码extname()lib/ManifestPlugin.js会把gz、br、map这类“复合扩展名”与前一节合并所以output.js.map的扩展名被识别为js.map而普通output.js的扩展名是js。再加上默认入口名为main、命名异步 chunk 名为async_js就得到清单里main.js/async_js.js/async_js.js.map等 key。而 value 中的file记录的则是去掉哈希前原始资源名或真实输出路径二者结合即可建立“稳定逻辑引用 → 带哈希真实 URL”的映射。ManifestPlugin 源码级工作原理示例背后是内置插件 lib/ManifestPlugin.jsPLUGIN_NAME ManifestPlugin。理解下面几个机制就能预判它在任意项目里的输出行为。1. 挂钩点processAssets 的 SUMMARIZE 阶段插件在compiler.hooks.thisCompilation中注册了compilation.hooks.processAssets的 tap并且显式指定阶段为Compilation.PROCESS_ASSETS_STAGE_SUMMARIZElib/ManifestPlugin.js。这意味着清单文件是在所有资源都已生成、进入“汇总”阶段后才计算的因而能拿到最终的文件名与哈希。2. 资源收集规则收集过程lib/ManifestPlugin.js按以下顺序遍历遍历compilation.chunks时先跳过HotUpdateChunk避免热更新补丁污染清单每个 chunk 先收录chunk.auxiliaryFiles即foo.txt、bar.txt这类伴随资源再收录chunk.files主输出文件最后遍历compilation.getAssets()跳过带hotModuleReplacement标记的热更新资源兜底收录其余独立产出如manifest.json自身、source map。3. 逻辑名的兜底回退当资源不是来自 chunk、也没有sourceFilename时比如第三方插件额外 emit 的文件插件会退回到“从文件名里剥掉哈希”的策略removeHashlib/ManifestPlugin.js。哈希正则[a-f0-9]{hashDigestLength,32}的位数由output.hashDigestLength决定因此index.XXXX.html可以被还原成index.html——这正是 html-webpack-plugin 等非官方插件产物的兼容处理。4. 最终输出所有条目写入后可选地经过generate改写整个对象再经serialize序列化最后通过compilation.emitAsset以RawSource形式、带{ manifest: true }信息 emit 到输出目录lib/ManifestPlugin.js。文件名取options.filename缺省为manifest.json。消费端按入口名收集全部首屏 JS 与 CSS清单生成的最终目的是服务运行时。template.md 提供了一个非常典型的消费函数——根据入口名把该入口及其父入口涉及的所有初始脚本与样式递归收集出来。下面先原样复述文档给出的实现const fs require(fs); function importEntrypoints(manifest, name) { const seen new Set(); function getImported(entrypoint) { const scripts []; const styles []; for (const item of entrypoint.imports) { const importer manifest.assets[item]; if (seen.has(item)) { continue; } seen.add(item); for (const parent of entrypoint.parents || []) { const [parentStyles, parentScripts] getImported(manifest.entrypoints[parent]) styles.push(...parentStyles); scripts.push(...parentScripts); } if (/\.css$/.test(importer.file)) { styles.push(importer.file); } else { scripts.push(importer.file); } } return [styles, scripts]; } return getImported(manifest.entrypoints[name]); } const manifest JSON.parser(fs.readFilsSync(./manifest.json, utf8)); // Get all styles and scripts by entry name const [styles, scripts] importEntrypoints(manifest, main);需要说明的是文档这段示例中存在两处笔误JSON.parser应为JSON.parse、fs.readFilsSync应为fs.readFileSync原样复制无法直接运行。它在逻辑上的核心思路仍然清晰且值得学习通过entrypoints.name.imports拿到逻辑资源 key如main.js用manifest.assets[item]反查得到真实文件路径file用seen集合做去重防止同一资源被重复收集通过entrypoint.parents || []递归处理父入口的依赖适用于 webpack 的“依赖入口”多入口嵌套场景对应 WebpackOptions.d.ts 中 entry 的dependOn能力按扩展名把.css归入样式、其余归入脚本。下面给出修掉笔误、并补齐遗漏返回与分号的“可直接运行”版本const fs require(fs); function importEntrypoints(manifest, name) { const seen new Set(); function getImported(entrypoint) { const scripts []; const styles []; for (const item of entrypoint.imports) { if (seen.has(item)) continue; seen.add(item); const importer manifest.assets[item]; for (const parent of entrypoint.parents || []) { const [parentStyles, parentScripts] getImported( manifest.entrypoints[parent] ); styles.push(...parentStyles); scripts.push(...parentScripts); } if (/\.css$/.test(importer.file)) { styles.push(importer.file); } else { scripts.push(importer.file); } } return [styles, scripts]; } return getImported(manifest.entrypoints[name]); } const manifest JSON.parse(fs.readFileSync(./manifest.json, utf8)); // Get all styles and scripts by entry name const [styles, scripts] importEntrypoints(manifest, main); console.log(styles:, styles); console.log(scripts:, scripts);在本文示例产物上把这段脚本放到构建输出目录与manifest.json同级运行传入main脚本列表会得到dist/output.js对应逻辑 keymain.js。若入口同时加载 CSS比如把 CSS 也打进 chunk.files.css分支就会把样式文件收集进styles。这套模式与 html-webpack-plugin 读取 manifest 后向 HTML 注入script/link的思路完全同源。两种模式下的构建输出对比示例文档还保留了该例在两种模式下的完整构建日志可用于验证 manifest 插件自身的工作与最终产物的差异。Unoptimized开发/默认模式assets by info 881 bytes [immutable] asset async_js.0eeb6882e0cf674fd1fc.js 873 bytes [emitted] [immutable] 1 related asset asset 3ee037f347c64cc372ad18857b0db91f.txt 4 bytes [emitted] [immutable] [from: foo.txt] (auxiliary name: main) asset a0145fafc7fab801e574.txt 4 bytes [emitted] [immutable] [from: bar.txt] (auxiliary name: main) asset output.js 14.6 KiB [emitted] (name: main) 1 related asset asset manifest.json 601 bytes [emitted] asset manifest.yml 395 bytes [emitted] chunk (runtime: main) async_js.0eeb6882e0cf674fd1fc.js 24 bytes [rendered] ./async.js ./example.js 6:8-28 ./async.js 24 bytes [built] [code generated] [exports: default] [used exports unknown] import() ./async.js ./example.js 6:8-28 chunk (runtime: main) output.js (main) 325 bytes (javascript) 4 bytes (asset) 7.41 KiB (runtime) [entry] [rendered] ./example.js main runtime modules 7.41 KiB 9 modules dependent modules 4 bytes (asset) 122 bytes (javascript) [dependent] 2 modules ./example.js 203 bytes [built] [code generated] [exports: default] [used exports unknown] entry ./example.js main webpack X.X.X compiled successfullyProduction 模式assets by path *.js 3.23 KiB asset output.js 3.05 KiB [emitted] [minimized] (name: main) 1 related asset asset async_js.59a751e1b9b97bdbc720.js 184 bytes [emitted] [immutable] [minimized] 1 related asset asset manifest.json 507 bytes [emitted] asset manifest.yml 309 bytes [emitted] asset 3ee037f347c64cc372ad18857b0db91f.txt 4 bytes [emitted] [immutable] [from: foo.txt] (auxiliary name: main) chunk (runtime: main) async_js.59a751e1b9b97bdbc720.js 24 bytes [rendered] ./async.js ./example.js 6:8-28 ./async.js 24 bytes [built] [code generated] [exports: default] import() ./async.js ./example.js 6:8-28 chunk (runtime: main) output.js (main) 283 bytes (javascript) 7.69 KiB (runtime) [entry] [rendered] ./example.js main runtime modules 7.69 KiB 9 modules dependent modules 80 bytes [dependent] 1 module ./example.js 203 bytes [built] [code generated] [exports: default] [no exports used] entry ./example.js main webpack X.X.X compiled successfully两段日志可以印证的关键事实两份清单manifest.json、manifest.yml在两种模式下都作为独立 asset 被 emit模式无关插件本身不参与压缩、不随生产优化被跳过入口 chunk 统一输出为output.js即(name: main)主 chunk异步 chunk 则因contenthash不同在两种模式下拥有不同文件名开发0eeb6882…、生产59a751e1…这正是清单必须存在的理由——文件名带哈希后任何硬编码 HTML 都会在每次发布后失效只有通过 manifest 在运行时动态解析才能拿到正确 URL同一份foo.txt内容仅foo两种模式下哈希一致3ee037…说明资源文件哈希只取决于内容与构建模式无关production 模式下 chunk 都带[minimized]标记、输出体积显著变小如主 chunk 由 14.6 KiB 缩到 3.05 KiB清单体积也随之减小manifest.json 601 → 507 bytes。参考资料仓库内深入路径文档模板与渲染产物examples/manifest-plugin/template.md、examples/manifest-plugin/README.md示例源码与配置example.js、async.js、webpack.config.js插件核心实现lib/ManifestPlugin.js选项 schema 与类型声明schemas/plugins/ManifestPlugin.json、declarations/plugins/ManifestPlugin.d.ts示例模板渲染机制examples/template-common.js、examples/buildAll.js若要把这套能力迁移到真实项目只需在webpack.config.js中new webpack.ManifestPlugin(...)webpack已内置该插件无需额外安装第三方包并让后端或 CI 在发布阶段读取 manifest 完成 URL 注入即可。需要留意的是本示例面向 Node 环境运行示例工程实际引入时建议先对照 schema 确认项目使用的 webpack 版本中选项含义是否一致。【免费下载链接】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),仅供参考