entry-dependencies-plugin
一、认识
EntryDependenciesPlugin 插件的收集 Entry 依赖的所有构建产物。包括入口自身构建产物、SplitChunk 拆分后的产物、通过 import() 动态加载的产物。其中难点在于如何收集 import() 动态加载的产物。以下是主要实现:
-
apply(compiler) 方法中, 注册 compiler.hooks.compilation 钩子, compilation 钩子在 Compiler 创建完 compilation 对象之后执行
-
在 compiler.hooks.compilation 钩子中, 注册 compilation.hooks.processAssets 或者 compilation.hooks.afterProcessAssets 钩子, assets 是在资源 assets 处理过程中执行操作, 允许插件在资源生成和优化的不同阶段插入自定义逻辑。afterProcessAssets 钩子是在所有资源处理完成后触发的钩子, 它通常用于在资源处理完成后执行一些清理或最终操作。在资源处理过程中, 我们通过 compilation.entrypoints.get(entry) 获取当前 Entry 的模块对象。compilation.assets 表示构建输出的所有产物, 是一个对象, 通常需要 Object.keys(compilation.assets) 获取构建输出的所有产物路径。通过 entryModule.getFiles() 获取当前入口所有依赖的正常构建产物路径。接下来获取通过 SplitChunks 拆分的产物路径, SplitChunks 拆分出来的产物我们认为是所有 Entry 共有的, 因此, 需要为每一个 Entry 都获取。通过 Webpack 配置中的 splitChunks.cacheGroups 获取拆分的 chunk 配置, 遍历每个 splitChunks.cacheGroup,从 Object.keys(compilation.assets) 中查找与该拆分 chunk 相关的文件(根据文件名匹配),查找出来的产物就是 SplitChunks 拆分的产物路径。最后就是找出 Entry 依赖的 通过 import() 动态加载的产物, Webpack 中没有提供直接 API, 所以, 通过 entryModule.chunks 获取 entryModule 所有 chunks, 遍历这些 chunks, 通过 entryChunk.getAllAsyncChunks() 获取所有动态加载的产物 chunks(注意: getAllAsyncChunks 获取的结果不是当前 Entry 的,是所有 Entry 的), 然后在遍历所有动态加载的产物 chunks, 每一个动态加载的 chunk 有一个 groupsIterable 属性, 对每个异步 chunk,通过 groupsIterable 获取它的所有 chunk group, 递归查找 chunkGroup.getParents 对应的父级依赖, 并从中提取每个父级的 id 或 name, 将每个父级的 id 或 name 添加到 entrypoints 集合中,如果当前父级没有 id 或 name,则继续向上查找父级。 父级就是每个 Entry。所以, 我们可以通过判断 Entry 是否包含在 entrypoints 来判断当前的 asyncChunk 是否为当前 Entry 的依赖。 ‘
-
compiler.hooks.afterEmit 是在所有文件已经写入输出目录后触发的钩子。它通常用于在文件写入完成后执行一些操作,例如生成额外的文件或执行一些后处理任务。所以,我们在 compiler.hooks.afterEmit 钩子中, 可以得到当前 Entry 所有依赖的产物路径, 包括入口自身构建产物、SplitChunk 拆分后的产物、通过 import() 动态加载的产物。
二、实现
const pluginName = "EntryDependenciesPlugin";
class EntryDependenciesPlugin {
constructor(options) {
this.options = options;
}
findEntrypoints(chunkGroup) {
const entrypoints = new Set();
const stack = [chunkGroup];
while (stack.length) {
const group = stack.pop();
const parents = group.getParents ? group.getParents() : [];
for (const parent of parents) {
const parentId = parent?.id || parent?.options?.id;
const parentName = parent?.name || parent?.options?.name;
if (parentName || parentId) {
entrypoints.add(parentName || parentId);
} else {
stack.push(parent);
}
}
}
return [...entrypoints];
}
getEntryAsyncFiles(entry, entryModule, allFiles) {
const entryAsyncFiles = new Set();
const entryChunks = entryModule.chunks;
entryChunks.forEach((entryChunk) => {
const asyncChunks = entryChunk.getAllAsyncChunks();
asyncChunks.forEach((asyncChunk) => {
asyncChunk.groupsIterable.forEach((chunkGroup) => {
const entryNames = this.findEntrypoints(chunkGroup);
const isIncludesEntry = entryNames.includes(entry);
const asyncChunkId = asyncChunk.id;
const asyncFiles = allFiles.filter((allFile) =>
allFile.includes(asyncChunkId)
);
if (isIncludesEntry && asyncFiles?.length) {
asyncFiles.forEach((asyncFile) => entryAsyncFiles.add(asyncFile));
}
});
});
});
return entryAsyncFiles;
}
getSplitChunkFiles(compiler, allFiles) {
const splitChunksFiles = new Set();
const splitChunksCacheGroups = Object.keys(
compiler.options?.optimization?.splitChunks?.cacheGroups || {}
);
splitChunksCacheGroups.forEach((splitChunksCacheGroup) => {
const allFilesFilters = allFiles.filter((allFile) =>
allFile.includes(splitChunksCacheGroup)
);
allFilesFilters.forEach((allFilesFilter) =>
splitChunksFiles.add(allFilesFilter)
);
});
return splitChunksFiles;
}
apply(compiler) {
const { entry } = this.options;
compiler.hooks.compilation.tap(pluginName, (compilation) => {
compilation.hooks.afterProcessAssets.tap(pluginName, (assets) => {
const entryModule = compilation.entrypoints.get(entry);
if (!entryModule) {
return;
}
const allFiles = Object.keys(assets);
const entryNormalFiles = new Set([...entryModule.getFiles()]);
const splitChunksFiles = this.getSplitChunkFiles(compiler, allFiles);
const entryAsyncFiles = this.getEntryAsyncFiles(
entry,
entryModule,
allFiles
);
this.entryFiles = new Set([
...entryAsyncFiles,
...entryNormalFiles,
...splitChunksFiles,
]);
});
});
compiler.hooks.afterEmit.tap(pluginName, ()=>{
console.log("this.entryFiles", [...this.entryFiles])
});
}
}
module.exports = EntryDependenciesPlugin;
三、配置
const { merge } = require("webpack-merge");
const common = require("./webpack.config.common");
const EntryDependenciesPlugin = require("./plugin/entryDependenciesPlugin");
module.exports = merge(common, {
mode: "production",
devtool: false,
plugins: [
new EntryDependenciesPlugin({ entry: "pageA" }),
new EntryDependenciesPlugin({ entry: "pageB" }),
new EntryDependenciesPlugin({ entry: "pageC" }),
],
});