webpack 5 如何用 ManifestPlugin 生成构建产物清单并自定义过滤与生成逻辑
【免费下载链接】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
如果你的 webpack 5 项目构建后产出了多个 chunk、拷贝资源(foo.txt之类的静态文件)和 source map,而你的部署脚本、CDN 同步任务或 SSR 服务端需要一份“这次构建到底输出了哪些文件”的清单,ManifestPlugin就是 webpack 内置的解决方案:它在每次编译结束时生成一份包含entrypoints(各入口引入的文件)和assets(产物文件名到源文件/产物路径的映射)的清单,并通过filter、generate、serialize三个钩子函数让你自定义“哪些文件进清单、清单里再加什么字段、最终写成什么格式”。官方示例位于 examples/manifest-plugin,本文基于该示例与 lib/ManifestPlugin.js 的实现整理。以下命令与配置均可在 webpack 5(本仓库当前版本为 5.110.3)下运行。
准备:示例的入口与产物构成
官方示例的入口 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处理)、new URL引用的bar.txt、入口 chunk(output.js)、动态import("./async.js")产生的异步 chunk,以及开启devtool: "source-map"后的 source map 文件。示例目录 examples/manifest-plugin 中还包含 async.js、foo.txt、bar.txt,实际搭建时把这几个文件一并复制即可。
清单中每一项的含义定义在 declarations/plugins/ManifestPlugin.d.ts:
assets:Record<string, ManifestItem>,每项为{ file, src? }——file是产物路径(经prefix处理后为绝对 URL 形式),src是相对 context 的源文件路径;entrypoints:每个入口对应{ imports: string[], parents?: string[] },imports是该入口引入的文件名,parents是父入口名(存在时才会出现)。
基本用法:在 plugins 中注册 ManifestPlugin
示例配置 examples/manifest-plugin/webpack.config.js 同时注册了两个ManifestPlugin实例,分别输出manifest.json和经过完全自定义的manifest.yml。第一个实例只传了filename,用来展示默认行为:
"use strict"; // @ts-expect-error no types for yamljs const YAML = require("yamljs"); const webpack = require("webpack"); // 示例仓库内写作 require("../../"),独立项目中替换为 "webpack" /** @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;运行构建需要webpack、file-loader和yamljs(后者仅第二个实例的serialize用到;仓库中该require带@ts-expect-error no types for yamljs注释,因为yamljs没有类型声明)。在独立项目中安装这三个依赖后,用webpack-cli按上述配置执行一次构建即可。
默认值与行为在 lib/ManifestPlugin.js 中:filename不传时输出到output.path下的manifest.json(DEFAULT_FILENAME);prefix不传时使用[publicpath](DEFAULT_PREFIX);serialize不传时用JSON.stringify(manifest, null, 2)。
六个配置项:filename、prefix、filter、generate、serialize、entrypoints
options 的 JSON Schema 对全部字段给出了解释,汇总如下:
| 选项 | 类型 | 作用(依据 Schema 描述与实现) |
|---|---|---|
filename | string | 输出文件名,默认manifest.json,写入output.path目录 |
prefix | string | 为清单中每个条目的file添加路径前缀;其中[publicpath]占位符会被替换为output.publicPath(publicPath为"auto"时替换为/,见 lib/ManifestPlugin.js) |
filter | function | 过滤进入清单的文件;对每个条目({ file, src? })返回false即剔除 |
generate | function | 接收 manifest 对象、修改后返回,用于增删字段 |
serialize | function | 接收 manifest 对象,返回最终写入文件的字符串 |
entrypoints | boolean | 开关entrypoints段落,默认为开启(lib/ManifestPlugin.js) |
filter收到的是已经拼好prefix的条目对象,示例里用它剔除所有.map结尾的 source map 文件。generate在filter之后、序列化之前执行,示例里做了两件事:删掉manifest.json自身(第一个插件输出的清单文件也会出现在编译产物中,第二个清单不需要记录它),以及注入自定义字段custom: "value"——ManifestObject允许additionalProperties,所以可以任意扩展。
执行构建并核对输出
示例文档中给出的dist/manifest.json如下(文档示例输出,其中.txt资产的 contenthash 文件名会随构建内容变化):
{ "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" } } }而应用了prefix/filter/generate/serialize的dist/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逐项核对四个自定义点,可以确认每个钩子都生效了:
prefix: "/nested/[publicpath]"——所有file值前多出了/nested/,说明[publicpath]被解析成了配置中对应的publicPath;filter——output.js.map和async_js.js.map没有出现在assets里,而 JSON 版本(未过滤)保留了它们;generate——assets里没有manifest.json条目(示例删除的是第一个清单文件名),且顶层多了custom: value;serialize——输出是 YAML 而非 JSON,缩进为 4 空格,对应YAML.stringify(manifest, 4)。
构建成功时,webpack 的 stats 输出中会列出清单文件本身。示例文档展示了 Unoptimized 与 Production mode 两种情况的 stats(文档示例输出),两者都能看到类似这样的行,说明清单文件已作为资产写入磁盘:
asset manifest.json 601 bytes [emitted] asset manifest.yml 395 bytes [emitted] webpack X.X.X compiled successfully也就是说,验证方式就是构建成功后检查output.path下是否存在manifest.json/manifest.yml,其内容结构符合上面的entrypoints+assets形状。
用清单还原入口的初始脚本与样式
当清单用于给 HTML 模板或服务端渲染收集入口需要的资源时,示例 README 还给出了一个importEntrypoints辅助函数,按入口名递归解析entrypoints(含parents指向的父入口),并按.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) { 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.parse(fs.readFileSync("./manifest.json", "utf8")); // Get all styles and scripts by entry name const [styles, scripts] = importEntrypoints(manifest, "main");注意:仓库原文这里写作JSON.parser(fs.readFilsSync(...)),是笔误;上面已修正为JSON.parse和fs.readFileSync,复制使用时以修正后的为准。
边界与限制
- 清单只在
processAssets的PROCESS_ASSETS_STAGE_SUMMARIZE阶段生成(lib/ManifestPlugin.js),即所有编译产物就绪之后,assets里记录的就是本次编译实际发出的文件。 - 带
hotModuleReplacement信息的资产和HotUpdateChunk会被跳过,不出现在清单中(lib/ManifestPlugin.js)。 assets的键名优先级是:chunk 名 + 扩展名(如main.js)→ 资产的sourceFilename(如foo.txt)→ 从文件名中去掉 hash 的兜底值;所以静态文件在清单中通常以源文件名作键,并通过src字段指向源文件。- 示例配置中
optimization.chunkIds: "named"的作用在源码注释中写明:让不同 mode 下的文件名保持一致,便于对照清单。
进一步查看完整的输入输出对照,直接读 examples/manifest-plugin/README.md;选项定义参见 declarations/plugins/ManifestPlugin.d.ts 与 schemas/plugins/ManifestPlugin.json。
【免费下载链接】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),仅供参考