返回
ModuleFederationPlugin 源码解析(五)之揭秘神奇的共享模块机制
前端
2023-12-02 03:58:36
ModuleFederationPlugin 源码解析(五)之揭秘神奇的共享模块机制
在 上篇文章 中,我们已经了解了 ModuleFederationPlugin 的基本原理和使用方法。但还有一个小细节,却容易被我们所忽视,那就是共享模块机制。
共享模块机制是一个 Provide 和 Consume 模型,需要 ProvideSharedModule
和 ConsumeSharedPlugin
同时提供相关的功能才能保证模块加载机制。这两个 Plugin 的源码相对较简单,我们不妨先来看看 ProvideSharedModule
的源码:
class ProvideSharedModule extends Plugin {
constructor(options) {
super();
this.sharedModules = new Map();
this.options = options;
}
apply(compiler) {
const options = this.options;
const moduleIds = new Set();
compiler.hooks.compilation.tap('ProvideSharedModule', (compilation) => {
const compilationParams = compilation.getCompilationParams();
// 根据参数生成一个唯一的 shared module id
const { name, filename, request } = options;
const sharedModuleId = createSharedModuleId(name, filename, request, compilationParams.chunkGraph);
// 将共享模块 id 与模块本身保存在映射中
this.sharedModules.set(sharedModuleId, {
id: sharedModuleId,
name: name,
filename: filename,
request: request,
});
// 告诉 webpack 这个模块是共享的
moduleIds.add(sharedModuleId);
});
// 在生成模块图时,过滤掉共享模块
compiler.hooks.beforeModuleIds.tap('ProvideSharedModule', (moduleIds) => {
for (const sharedModuleId of this.sharedModules.keys()) {
moduleIds.delete(sharedModuleId);
}
});
// 在生成块图时,过滤掉共享模块
compiler.hooks.beforeChunks.tap('ProvideSharedModule', (chunks) => {
for (const sharedModuleId of this.sharedModules.keys()) {
for (const chunk of chunks) {
const idx = chunk.modules.indexOf(sharedModuleId);
if (idx !== -1) {
chunk.modules.splice(idx, 1);
}
}
}
});
// 在代码生成阶段,为共享模块生成代码
compiler.hooks.thisCompilation.tap('ProvideSharedModule', (compilation) => {
const outputOptions = compilation.options.output;
const { publicPath, path: outputPath } = outputOptions;
for (const sharedModule of this.sharedModules.values()) {
const moduleSource = this.generateSharedModuleSource(sharedModule, outputOptions);
const { filename } = sharedModule;
const absoluteFilename = path.join(outputPath, filename);
compilation.emitAsset(filename, new RawSource(moduleSource));
// 将共享模块的 public path 添加到 compilation 的 public paths 中
compilation.addSharedEntry({
name: sharedModule.name,
filename: filename,
publicPath: publicPath,
// 这里将 source 用到了,但实际意义是什么,不太清楚
source: () => this.generateSharedModuleSource(sharedModule, outputOptions),
});
}
});
}
// 生成共享模块的代码
generateSharedModuleSource(sharedModule, outputOptions) {
const { name, filename, request } = sharedModule;
const publicPath = outputOptions.publicPath;
const relativePath = path.relative(outputOptions.path, filename);
return `
var __webpack_exports__ = window[${JSON.stringify(name)}] = window[${JSON.stringify(
name
)}] || {};
(window[${JSON.stringify(request)}] = function() {
__webpack_exports__ = __webpack_exports__ || {};
var module = {
exports: __webpack_exports__
};
var __webpack_require__ = function(moduleId) {
if(!__webpack_exports__[moduleId]) {
var module = __webpack_modules__[moduleId];
if(!module) throw new Error('Cannot find module \'' + moduleId + '\'');
module.call(module.exports, module, module.exports, __webpack_require__);
}
return __webpack_exports__[moduleId];
};
// 加载共享模块的代码
${readFileSync(request, 'utf-8')}
return __webpack_exports__;
})()
`;
}
}
从 ProvideSharedModule
的源码中,我们可以看到,它主要做了以下几件事:
- 在
apply
方法中,根据传入的选项生成一个唯一的共享模块 id,并将共享模块 id 与模块本身保存在映射中。 - 在
beforeModuleIds
和beforeChunks
方法中,过滤掉共享模块,以免它们被 webpack 打包进最终的 bundle 中。 - 在
thisCompilation
方法中,为共享模块生成代码并将其添加到 compilation 的 public paths 中。
接下来,我们再来看看 ConsumeSharedPlugin
的源码:
class ConsumeSharedPlugin extends Plugin {
constructor(options) {
super();
this.options = options;
}
apply(compiler) {
const options = this.options;
compiler.hooks.compilation.tap('ConsumeSharedPlugin', (compilation) => {
const compilationParams = compilation.getCompilationParams();
// 根据参数生成一个唯一的 shared module id
const { name, filename, request } = options;
const sharedModuleId = createSharedModuleId(name, filename, request, compilationParams.chunkGraph);
// 告诉 webpack 这个模块是共享的
compilation.addSharedModule({
name: name,
filename: filename,
request: request,
});
// 在代码生成阶段,将共享模块的代码添加到编译结果中
compiler.hooks.thisCompilation.tap('ConsumeSharedPlugin', (compilation) => {
const outputOptions = compilation.options.output;
const { publicPath, path: outputPath } = outputOptions;
const relativePath = path.relative(outputOptions.path, filename);
const moduleSource = `
window[${JSON.stringify(name)}] = window[${JSON.stringify(
name
)}] || window[${JSON.stringify(request)}](\'' + ${JSON.stringify(publicPath)} + ${JSON.stringify(
relativePath
)} + '\');
`;
compilation.emitAsset(filename, new RawSource(moduleSource));
});
});
}
}
从 ConsumeSharedPlugin
的源码中,我们可以看到,它主要做了以下几件事:
- 在
apply
方法中,根据传入的选项生成一个唯一的共享模块 id,并将共享模块 id 添加到 compilation 的 shared modules 中。 - 在
thisCompilation
方法中,将共享模块的代码添加到编译结果中。
通过以上两个 Plugin 的源码分析,我们可以发现,共享模块机制其实很简单,就是将共享模块从最终的 bundle 中分离出来,并将其单独打包成一个文件。然后,在需要使用共享模块的模块中,通过 require
的方式加载共享模块的文件。这样,就可以实现共享模块的复用,从而减少代码的体积和提高加载速度。