想让一个自定义文本文件可以 import,可以写 Loader;想把本次构建输出的资源列成清单,可以写 Plugin。这两个需求正好能把它们参与构建的位置区分开。
下面把 .note 文本变成 JavaScript 模块,再输出 assets.json。
在空目录运行 npm init -y 和 npm install —save-dev —save-exact webpack@5.99.9。下面用 Node API 构建,不依赖全局 webpack 命令。
Loader 返回一段模块源码
创建 text-loader.cjs:
module.exports = function (source) {
this.cacheable();
return "export default " + JSON.stringify(source.trim());
};
输入是文本,输出是 export default 后面的字符串。JSON.stringify 处理引号和换行,不能直接把原文夹进引号里拼代码。trim 是这个小例子的格式约定,想保留首尾空白就去掉它。
创建 message.note,内容是“一段经过 Loader 的文字”。创建 entry.js:
import text from "./message.note";
globalThis.loaderResult = text;
Plugin 在资源阶段生成清单
创建 AssetsPlugin.cjs:
module.exports = class AssetsPlugin {
apply(compiler) {
compiler.hooks.thisCompilation.tap("AssetsPlugin", (compilation) => {
compilation.hooks.processAssets.tap(
{
name: "AssetsPlugin",
stage: compiler.webpack.Compilation.PROCESS_ASSETS_STAGE_REPORT,
},
() => {
const names = Object.keys(compilation.assets).sort();
compilation.emitAsset(
"assets.json",
new compiler.webpack.sources.RawSource(JSON.stringify(names, null, 2)),
);
},
);
});
}
};
apply 注册构建 hook,thisCompilation 拿到这次 compilation,再在 processAssets 里输出清单。资源交给 compilation.emitAsset 管理,不在 done 后绕过构建流程改磁盘文件。
REPORT 阶段已经能看到大部分已有资源,但如果别的插件更晚输出文件,清单不一定包含它。这里在 emitAsset 前取名字,所以不会把清单自己列进去。
创建 build.cjs:
const webpack = require("webpack");
const path = require("path");
webpack(
{
mode: "production",
entry: path.resolve("entry.js"),
output: {
path: path.resolve("dist"),
filename: "main.js",
chunkFilename: "[id].js",
},
devtool: false,
module: { rules: [{ test: /\.note$/, use: [path.resolve("text-loader.cjs")] }] },
plugins: [new (require("./AssetsPlugin.cjs"))()],
},
(error, stats) => {
if (error || stats.hasErrors()) {
console.error(error || stats.toString("errors-only"));
process.exitCode = 1;
return;
}
console.log(
JSON.stringify(
stats.toJson({
all: false,
assets: true,
chunks: true,
modules: true,
usedExports: true,
}),
null,
2,
),
);
},
);
运行 node build.cjs,再创建 check.cjs 并运行:
const fs = require("fs");
const vm = require("vm");
const assert = require("assert").strict;
const context = {};
vm.runInNewContext(fs.readFileSync("dist/main.js", "utf8"), context);
assert.equal(context.loaderResult, "一段经过 Loader 的文字");
const assets = JSON.parse(fs.readFileSync("dist/assets.json", "utf8"));
assert.ok(assets.includes("main.js"));
console.log("文本导入和资源清单都正常");
只看构建成功还不够。这里同时确认模块导入的值正确,清单也包含主文件。VM 执行的是自己写的测试产物,涉及 DOM 的应用不能直接照搬这个检查。
这个 Loader 没做异步读取、source map 或多阶段转换,Plugin 也不是完整的发布清单系统。遇到具体需求再补接口,比一开始做很多抽象容易看清输入和输出。接口参考 Loader 文档 和 processAssets 文档。