返回

ModuleFederationPlugin 源码解析(五)之揭秘神奇的共享模块机制

前端




ModuleFederationPlugin 源码解析(五)之揭秘神奇的共享模块机制

上篇文章 中,我们已经了解了 ModuleFederationPlugin 的基本原理和使用方法。但还有一个小细节,却容易被我们所忽视,那就是共享模块机制。

共享模块机制是一个 Provide 和 Consume 模型,需要 ProvideSharedModuleConsumeSharedPlugin 同时提供相关的功能才能保证模块加载机制。这两个 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 的源码中,我们可以看到,它主要做了以下几件事:

  1. apply 方法中,根据传入的选项生成一个唯一的共享模块 id,并将共享模块 id 与模块本身保存在映射中。
  2. beforeModuleIdsbeforeChunks 方法中,过滤掉共享模块,以免它们被 webpack 打包进最终的 bundle 中。
  3. 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 的源码中,我们可以看到,它主要做了以下几件事:

  1. apply 方法中,根据传入的选项生成一个唯一的共享模块 id,并将共享模块 id 添加到 compilation 的 shared modules 中。
  2. thisCompilation 方法中,将共享模块的代码添加到编译结果中。

通过以上两个 Plugin 的源码分析,我们可以发现,共享模块机制其实很简单,就是将共享模块从最终的 bundle 中分离出来,并将其单独打包成一个文件。然后,在需要使用共享模块的模块中,通过 require 的方式加载共享模块的文件。这样,就可以实现共享模块的复用,从而减少代码的体积和提高加载速度。