跳到主要内容

source-file-plugin

2024年12月21日
柏拉文
越努力,越幸运

一、认识


SourceFilePlugin: SourceFilePlugin 是一个自定义的 Webpack 插件,主要逻辑是在构建的 afterEmit 阶段生成一个包含每个入口模块资源信息的 JSON 文件。插件会收集并整理每个入口模块相关的 CSSJS 资源,并根据配置调整资源加载的优先级。生成的 source.json 文件会被写入到每个入口模块的文件夹中。这样,每个应用可以根据 source.json 中的资源列表,确定需要加载哪些资源,以及这些资源的加载顺序。

SourceFilePlugin 可以与 HtmlWebpackPlugininject 配置结合使用,inject 控制是否自动注入资源文件。当将 inject 设置为 false 时,HTML 文件不会自动注入任何资源。此时,我们可以通过 Node.js 模版分发服务,根据 source.json 中已经排序好的 JSCSS 资源列表,手动插入 <script><link> 标签,确保每个应用只加载所需资源,并避免资源冲突或重复加载。例如,CSS 文件的加载顺序可以根据优先级进行控制,避免样式覆盖;同时,子应用的资源可以按需加载,确保性能优化并减少冗余加载。

通过这种方式,我们也能够灵活控制微前端架构中资源的加载顺序和管理,确保每个应用的资源按需加载,避免重复加载,提升了资源管理的效率和应用性能。

二、实现


const path = require('path');
const fs = require('fs/promises');
const PLUGIN_NAME = 'BolawenSourceFilePlugin';

class BolawenSourceFilePlugin {
constructor(options = {}) {
this.options = {
unitName: "",
config: {},
...options
};
}

apply(compiler) {
const { config } = this.options;

compiler.hooks.afterEmit.tapPromise(PLUGIN_NAME, async (compilation) => {
const allEntries = Array.from(compilation.entries);

await Promise.all(allEntries.map(async ([unitName]) => {
const entryModule = compilation.entrypoints.get(unitName);
if (!entryModule) return;

const chunks = this.getChunks(entryModule);
const cssChunksOfPriority = this.getPrioritizedChunks(chunks, config.cssChunkPriorityMap);

const data = this.generateData(unitName, cssChunksOfPriority, chunks, config, compilation);

await this.writeFile(compilation, unitName, data);
}));
});
}

getChunks(entryModule) {
return entryModule.chunks.map((chunk) => ({
name: chunk.name,
files: Array.from(chunk.files)
}));
}

getPrioritizedChunks(chunks, cssChunkPriorityMap = {}) {
return chunks
.map((chunk) => ({
...chunk,
priority: cssChunkPriorityMap[chunk.name] || 0
}))
.sort((a, b) => a.priority - b.priority);
}

generateData(unitName, cssChunksOfPriority, chunks, config, compilation) {
return {
source_version: 1,
scope: [],
keys: [],
...config,
data: {
headers: {
css: cssChunksOfPriority.map((chunk) => ({
name: chunk.name,
files: chunk.files.filter((filePath) => /\.css$/.test(filePath))
}))
},
footers: {
js: chunks.map((chunk) => ({
name: chunk.name,
files: chunk.files.filter((filePath) => /\.js$/.test(filePath))
}))
},
public: compilation.outputOptions.publicPath,
},
};
}

async writeFile(compilation, unitName, data) {
const outputDir = path.join(compilation.compiler.outputPath, unitName);
const outputPath = path.join(outputDir, 'source.json');

try {
await fs.mkdir(outputDir, { recursive: true });
await fs.writeFile(outputPath, JSON.stringify(data, null, 2), { flag: 'w' });
} catch (error) {
console.error(`[${PLUGIN_NAME}] Error writing file:`, error);
}
}
}

module.exports = BolawenSourceFilePlugin;

三、配置


const SourceFilePlugin = require("./source-file-plugin.js")

module.exports = {
devtool: false,
mode: 'development',
entry: {
main: path.resolve(__dirname, './index.js')
},
output: {
filename: '[name].js',
path: path.resolve(__dirname, 'dist')
},
plugins: [
new SourceFilePlugin({
unitName: "pageA",
config: {
keys: [],
scope: [],
}
})
]
};

四、产物


{
"data": {
"headers":{
"css": [
{
"name": "pageA",
"files": ["pageA.css"]
}
]
},
"footers": {
"js": [
{
"name": "pageA",
"files": ["pageA.js"]
}
]
}
},
"public": "/bolawen",
"cssChunkPriorityMap":{
"common": 100
}
}