UMI 4 新特性源码解读系列三:MFSU

本文是 UMI 4 源码主题阅读的第三篇。有些同学可能对 UMI 4 新特性还不太熟悉,推荐看一下:www.yuque.com/antfe/featu...

背景

这里需要说明的是,MFSU 在 UMI 3 就已经有了,只不过当时 bug 比较多,在 UMI 4 中很多问题都得到了解决。

什么是 MFSU?借用官方文档中的描述:

MFSU 是一种基于 webpack5 新特性 Module Federation 的打包提速方案。其核心的思路是通过分而治之,将应用源代码的编译和应用依赖的编译分离,将变动较小的应用依赖构建为一个 Module Federation 的 remote 应用,以免去应用热更新时对依赖的编译

顺带说一句,本人在看文档的时候,发现了很多 typo,还发现一个链接挂掉,实在看不下去了,顺手给官方提了两个 PR:

github.com/umijs/umi/p...
github.com/umijs/umi/p...

一提到 Module Federation 有些同学可能会觉得比较陌生,这里不过多涉及 Module Federation 相关介绍,但是可以做个简单的类比,一般前端工程里面我们会通过 Code-Splitting 拆分 Async Chunk,但是这个 Async Chunk 只能在工程内部消费,无法共享给其他工程(这是 Webpack 打包机制决定的);Module Federation 作用就是跨工程共享 Async Chunk,解决代码共享、复用问题。

MFSU 做的事情其实就是扫描依赖,对依赖预打包,然后通过 Module Federation 共享给业务工程消费。业务工程无需对依赖进行构建、打包,进而提升了构建效率。有没有觉得这种设计思路很熟悉了,没错,Vite 也用到了依赖预构建。

官方文档介绍 MFSU 有两种策略,一种是 normal 策略 (编译时分析)。该策略在编译期间通过 Babel 插件修改业务工程代码的 import 路径,让路径指向 MF 的模块地址,同时做依赖收集。等业务工程编译完成后,继续进行依赖部分的代码构建。该策略可以收集到完整的依赖,但缺点是整个过程是串行的,比较耗时。

另一种是 eager 策略 (扫描方式)。该策略会预先读取业务工程源码文件,通过静态分析方式获取项目依赖。分析完依赖信息后,Umi 会拿着这份依赖信息,并行的去进行业务工程代码构建和依赖构建。该策略效率较高,但缺点是会漏掉编译期间动态插入的依赖,这部分依赖最终和业务工程代码一起编译打包。

介绍完整体流程,下面来看源码分析。

源码分析

由于 MFSU 涉及的链路比较多,这篇文章我们换一种方式,通过对照文档 API 用法一点点分析源码实现。

首先 MFSU 提供了一个单独的包,直接安装这个包:

bash 复制代码
pnpm add @umijs/mfsu -D

1. 初始化实例

首先需要初始化一个 MFSU 实例:

ts 复制代码
// webpack.config.js

const { MFSU } = require('@umijs/mfsu')
const webpack = require('webpack')

const mfsu = new MFSU({
  implementor: webpack,
  buildDepWithESBuild: true,
});

从上例中可以看出,MFSU 实际上就是一个 class,当我们用 new 操作,会执行构造方法:

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  constructor(opts: IOpts) {
    // 此处省略一些代码

    if (this.opts.strategy === 'eager') {
      if (opts.srcCodeCache) {
        // 当指定了 `strategy="eager"` 且指定了 `opts.srcCodeCache`
        // 初始化一个 `StaticAnalyzeStrategy` 实例赋值给 `this.strategy`
        logger.info('MFSU eager strategy enabled');
        this.strategy = new StaticAnalyzeStrategy({
          mfsu: this,
          srcCodeCache: opts.srcCodeCache,
        });
      } else {
        // 如果没有指定 `opts.srcCodeCache` 则 fallback 到 `strategy="normal"`
        logger.warn(
          'fallback to MFSU normal strategy, due to srcCache is not provided',
        );
        this.strategy = new StrategyCompileTime({ mfsu: this });
      }
    } else {
      // `strategy="normal"` 模式
      // 初始化 `StrategyCompileTime` 实例赋值给 `this.strategy`
      this.strategy = new StrategyCompileTime({ mfsu: this });
    }

    // 加载缓存,反序列化创建上次构建的 snapshot
    this.strategy.loadCache();

    // 初始化 `DepBuilder` 实例
    this.depBuilder = new DepBuilder({ mfsu: this });
  }
}

2. 添加中间件

第二步,添加 MFSUdevServer 中间件到 webpack-dev-server 中,他将为你提供 MFSU 所需打包后的资源:

ts 复制代码
// webpack.config.js

module.exports = {
  devServer: {
    setupMiddlewares(middlewares, devServer) {
      middlewares.unshift(
        ...mfsu.getMiddlewares()
      )
      return middlewares
    },
  }
}

getMiddlewares()MFSU 类的实例方法,我们来看一下实现:

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  // 此处省略一些代码

  getMiddlewares() {
    return [
      (req: Request, res: Response, next: NextFunction) => {
        // 解析静态资源的 publicPath
        const publicPath = this.publicPath;
        const relativePublicPath = isAbsoluteUrl(publicPath)
          ? new URL(publicPath).pathname
          : publicPath;
        // 获取 `req.path`,判断是否请求 MF 的资源
        const isMF =
          req.path.startsWith(`${relativePublicPath}${MF_VA_PREFIX}`) ||
          req.path.startsWith(`${relativePublicPath}${MF_DEP_PREFIX}`) ||
          req.path.startsWith(`${relativePublicPath}${MF_STATIC_PREFIX}`);
        if (isMF) {
          // 当前请求是 MF 资源,重写服务端响应逻辑
          // 监听 `depBuilder.onBuildComplete` 事件
          // 等待依赖编译结束响应静态资源(编译完成之前请求是挂起状态)
          this.depBuilder.onBuildComplete(() => {
            // 资源不是 `remoteEntry.js`,设置强缓存
            // 如果 `remoteEntry.js` 不可设置强缓存
            if (!req.path.includes(REMOTE_FILE)) {
              res.setHeader('cache-control', 'max-age=31536000,immutable');
            }
            // 根据资源类型设置 Content-Type
            res.setHeader(
              'content-type',
              lookup(extname(req.path)) || 'text/plain',
            );

            // 将 `req.path` 转换为依赖构建产物的文件系统路径
            const relativePath = req.path.replace(
              new RegExp(`^${relativePublicPath}`),
              '/',
            );
            const realFilePath = join(this.opts.tmpBase!, relativePath);

            // 如果文件不存在,打印错误信息并响应 404
            if (!existsSync(realFilePath)) {
              logger.error(`MFSU dist file: ${realFilePath} not found`);
              if (this.lastBuildError) {
                logger.error(`MFSU latest build error: `, this.lastBuildError);
              }

              res.status(404);
              return res.end();
            }

            // 文件存在,响应文件内容
            const content = readFileSync(realFilePath);
            res.send(content);
          });
        } else {
          // 当前请求不是 MF,用默认响应逻辑
          next();
        }
      },
      // 兜底依赖构建时, 代码中有指定 chunk 名的情况
      express.static(this.opts.tmpBase!),
    ];
  }
}

3. 配置转换器

第三步,你需要配置一种源码转换器,他的作用是用来收集、转换依赖导入路径,替换为 MFSU 的模块联邦地址(中间件所提供的)。

此处提供两种方案:babel pluginsesbuild handler,一般情况下选择 babel plugins 即可。

ts 复制代码
// webpack.config.js

module.exports = {
  module: {
    rules: [
      {
        test: /\.[jt]sx?$/,
        exclude: /node_modules/,
        use: {
          loader: 'babel-loader',
          options: {
            plugins: [
              ...mfsu.getBabelPlugins()
            ]
          }
        }
      }
    ]
  }
}

getBabelPlugins 也是 MFSU 类的实例方法,简单看一下实现:

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  // 此处省略一些代码

  getBabelPlugins() {
    // 根据不同策略,需要加载不同 Babel 插件
    return [this.strategy.getBabelPlugin()];
  }
}

4. 设定 webpack 配置

第四步,调用 MFSU 提供的方法改变你的 webpack 配置,在这里只有增量行为,你无需担心会影响到你原来的配置内容。

如下代码所示,mfsu.setWebpackConfig 是一个异步方法,为了调用他你需要将原来的 webpack 配置单独抽为一个对象 config 之后,再将调用此方法的返回值导出。

ts 复制代码
// webpack.config.js

const config = {
  // origin webpack config
}

const depConfig = {
  // webpack config for dependencies
}

const getConfig = async () => {
  await mfsu.setWebpackConfig({
    config, depConfig
  });
  return config
}

module.exports = getConfig()

这里核心就是 mfsu.setWebpackConfig() 方法,里面逻辑比较多,我们分两块看。第一部分主要遍历 Webpack 入口配置,收集 importexport,生成新的虚拟模块和入口,替换原先 Webpack 的入口配置:

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  async setWebpackConfig(opts: {
    config: Configuration;
    depConfig: Configuration;
  }) {
    const { mfName } = this.opts;
    // 给 Babel 插件适配 `this.alias` 和 `this.externals`
    // 有些 `import` 语句是基于 `alias` 配置实现的,Babel 插件需要解析正确路径
    // 还有一些 `import` 配置了 `externals`,不需要收集依赖
    Object.assign(this.alias, opts.config.resolve?.alias || {});
    this.externals.push(...makeArray(opts.config.externals || []));
    // 收集入口和虚拟模块
    const entry: Record<string, string | string[]> = {};
    const virtualModules: Record<string, string> = {};
    // 将 `entry` 转为对象,方便进行遍历
    const entryObject = lodash.isString(opts.config.entry)
      ? { default: [opts.config.entry] }
      : (opts.config.entry as Record<string, string[]>);
    for (const key of Object.keys(entryObject)) {
      // 如果是项目导出的远端模块 不需要处理成动态加载的模块 以避免加载错误
      if (key === this.opts.remoteName) {
        entry[key] = entryObject[key];
        continue;
      }

      const virtualPath = `./${VIRTUAL_ENTRY_DIR}/${key}.js`;
      const virtualContent: string[] = [];
      let index = 1;
      let hasDefaultExport = false;
      const entryFiles = lodash.isArray(entryObject[key])
        ? entryObject[key]
        : ([entryObject[key]] as unknown as string[]);

      const resolver = getResolver(opts.config);
      for (let entry of entryFiles) {
        entry = resolver(entry);

        // 读取入口文件内容
        const content = readFileSync(entry, 'utf-8');
        // 解析入口文件的 `import` 和 `export`
        // 个人认为前端业务工程入口文件一般只有 `import` 没有 `export`
        const [_imports, exports] = await parseModule({ content, path: entry });
        if (exports.length) {
          // 生成 `import` 虚拟模块代码
          virtualContent.push(`const k${index} = ${this.asyncImport(entry)}`);
          for (const exportName of exports) {
            // 如果存在 `export` 则生成 `export` 的虚拟模块代码
            if (exportName === 'default') {
              hasDefaultExport = true;
              virtualContent.push(`export default k${index}.${exportName}`);
            } else {
              virtualContent.push(
                `export const ${exportName} = k${index}.${exportName}`,
              );
            }
          }
        } else {
          // 生成 `import` 虚拟模块代码
          virtualContent.push(this.asyncImport(entry));
        }
        index += 1;
      }
      // 如果没有默认导出,则添加一个
      if (!hasDefaultExport) {
        virtualContent.push(`export default 1;`);
      }
      // 拼接虚拟模块代码,收集到 `virtualModules`
      virtualModules[virtualPath] = virtualContent.join('\n');
      // 收集虚拟模块入口
      entry[key] = virtualPath;
    }
    // 将 Webpack 的入口配置替换为虚拟模块入口
    opts.config.entry = entry;

    // 此处省略一些代码
  }
}

第二部分主要是配置 Webpack 插件,其中就包括 ModuleFederationPlugin

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  async setWebpackConfig(opts: {
    config: Configuration;
    depConfig: Configuration;
  }) {
    // 此处省略一些代码

    // 确保 plugins 是数组,方便下面进行遍历
    opts.config.plugins = opts.config.plugins || [];

    // 从 Webpack 配置中获取 `publicPath`
    let publicPath = resolvePublicPath(opts.config);
    this.publicPath = publicPath;

    opts.config.plugins!.push(
      ...[
        // 使用 `WebpackVirtualModules` 插件
        // 将上面拼接的字符串代码生成虚拟模块
        // https://www.npmjs.com/package/webpack-virtual-modules
        new WebpackVirtualModules(virtualModules),
        // 从初始化阶段传入的 `implementor` 拿到 Webpack 实例
        // 基于 Webpack 实例创建 `ModuleFederationPlugin`
        // 注意这边 `ModuleFederationPlugin` 只配置了 `remotes` 用于消费远程 Async Chunk
        // 没有配置 `exposes` 将模块暴露给其他工程
        new this.opts.implementor.container.ModuleFederationPlugin({
          name: '__',
          // 支持在初始化阶段传入 `ModuleFederationPlugin` 的 `shared` 配置
          shared: this.opts.shared || {},
          remotes: {
            // 加载远程 remoteEntry.js
            [mfName!]: this.opts.runtimePublicPath
              ? // ref:
                // https://webpack.js.org/concepts/module-federation/#promise-based-dynamic-remotes
                `
promise new Promise(resolve => {
  const remoteUrlWithVersion = (window.publicPath || '/') + '${REMOTE_FILE_FULL}';
  const script = document.createElement('script');
  script.src = remoteUrlWithVersion;
  script.onload = () => {
    // the injected script has loaded and is available on window
    // we can now resolve this Promise
    const proxy = {
      get: (request) => window['${mfName}'].get(request),
      init: (arg) => {
        try {
          return window['${mfName}'].init(arg);
        } catch(e) {
          console.log('remote container already initialized');
        }
      }
    }
    resolve(proxy);
  }
  // inject this script with the src set to the versioned remoteEntry.js
  document.head.appendChild(script);
})
                `.trimLeft()
              : `${mfName}@${publicPath}${REMOTE_FILE_FULL}`, // mfsu 的入口文件如果需要在其他的站点上被引用,需要显示的指定publicPath,以保证入口文件的正确访问
          },
        }),
        new BuildDepPlugin(this.strategy.getBuildDepPlugConfig()),
      ],
    );

    // 给 Webpack 配置启用 topLevelAwait
    lodash.set(opts.config, 'experiments.topLevelAwait', true);

    // 从 `setWebpackConfig` 入参拿到依赖构建配置
    this.depConfig = opts.depConfig;

    // 注意这里的 `init` 方法只在 `StaticAnalyzeStrategy` 策略中用到
    // 当启用 `StrategyCompileTime` 插件该方法为 noop
    this.strategy.init(opts.config);
  }
}

以上就是 MFSU 的整体配置流程。但是我们注意到,在 MFSU 类中还有一个非常重要的 buildDeps 方法,该方法在配置阶段并没有调用,那显然需要通过 Webpack 插件,在打包构建阶段监听特定事件钩子实现调用。下面来看依赖构建流程。

5. 依赖构建流程

这部分逻辑有点绕,花了点时间理清楚了。在上面的代码中,配置了一个自定义 Webpack 插件:

ts 复制代码
new BuildDepPlugin(this.strategy.getBuildDepPlugConfig())

StrategyCompileTime 为例,我们来看下 getBuildDepPlugConfig 方法的逻辑:

ts 复制代码
// /packages/mfsu/src/mfsu/strategyCompileTime.ts

export class StrategyCompileTime implements IMFSUStrategy {
  // 此处省略一些代码

  getBuildDepPlugConfig(): IBuildDepPluginOpts {
    const mfsu = this.mfsu;

    return {
      onCompileDone: () => {
        if (mfsu.depBuilder.isBuilding) {
          mfsu.buildDepsAgain = true;
        } else {
          mfsu
            .buildDeps()
            .then(() => {
              mfsu.onProgress({
                done: true,
              });
            })
            .catch((e: Error) => {
              printHelp.runtime(e);
              mfsu.onProgress({
                done: true,
              });
            });
        }
      },
    };
  }
}

可以看到,该方法就是返回了一个配置对象,对象上带有 onCompileDone 事件钩子,当该事件钩子调用的时候,会执行 mfsu.buildDeps()。因此我们猜测,当 Webpack 构建完成的时候,此时依赖收集也完成了,然后执行 mfsu.buildDeps() 进行依赖构建,依赖构建结束调用 mfsu.onProgress({ done: true }) 说明整个 MFSU 流程已经完成。

我们先看一下 BuildDepPlugin 插件的代码,对于熟悉 Webpack 插件的同学来说这块应该非常简单:

ts 复制代码
// /packages/mfsu/src/webpackPlugins/buildDepPlugin.ts

export class BuildDepPlugin {
  private opts: IBuildDepPluginOpts;

  constructor(opts: IBuildDepPluginOpts) {
    this.opts = opts;
  }

  apply(compiler: Compiler): void {
    compiler.hooks.watchRun.tapPromise(PLUGIN_NAME, (c: Compiler) => {
      return this.opts.onFileChange?.(c) || Promise.resolve();
    });

    compiler.hooks.beforeCompile.tap(PLUGIN_NAME, () => {
      if (this.opts.beforeCompile) {
        return this.opts.beforeCompile?.();
      } else {
        return Promise.resolve();
      }
    });

    // 监听 `done` 事件,触发 `onCompileDone` 钩子
    compiler.hooks.done.tap(PLUGIN_NAME, (stats: Stats) => {
      if (!stats.hasErrors()) {
        this.opts.onCompileDone();
      }
    });
  }
}

上例代码 beforeCompileonFileChange 用于 StaticAnalyzeStrategy 策略,onCompileDone 用于 StrategyCompileTime 策略

下面重点就是 MFSUbuildDeps 方法:

ts 复制代码
// /packages/mfsu/src/mfsu/mfsu.ts

class MFSU {
  // 此处省略一些代码

  async buildDeps() {
    try {
      // 基于上次构建的 snapshot 判断依赖是否需要构建
      // 主要基于两个信息:
      // - `getCacheDependency` 为用户自定义函数,用返回值来对比,使 MFSU cache 无效的函数
      // - `ModuleGraph` 用来保存编译期间收集的依赖信息,是内部实现的一个数据结构
      // 首次创建 `MFSU` 实例的时候会通过 `loadCache` 方法从磁盘从恢复
      // 如果不需要返回 false,需要则返回 string 类型 reason
      const shouldBuild = this.strategy.shouldBuild();
      if (!shouldBuild) {
        logger.info('[MFSU] skip buildDeps');
        return;
      }

      // 获取此次构建最新的 snapshot,用于下次构建进行比对
      this.strategy.refresh();

      // 获取编译期间收集的依赖信息
      // `ModuleGraph` 是 `Map` 类型,前面调用 `refresh()` 方法获取 snapshot
      // 进一步调用 `snapshotDeps()`,会将 `ModuleGraph` 转为普通对象存到 `depSnapshotModules`
      // 调用 `getDepModules()` 方法直接返回 `depSnapshotModules`
      const staticDeps = this.strategy.getDepModules();

      // `buildDeps` 是静态方法,用于将每个依赖转换为 `Dep` 对象,返回数组
      const deps = Dep.buildDeps({
        deps: staticDeps,
        cwd: this.opts.cwd!,
        mfsu: this,
      });
      logger.info(`[MFSU] buildDeps since ${shouldBuild}`);
      logger.debug(deps.map((dep) => dep.file).join(', '));

      // 调用 `depBuilder.build` 方法实现依赖构建
      await this.depBuilder.build({
        deps,
      });
      this.lastBuildError = null;

      // 写入缓存到 .mfsu/MFSU_CACHE.json
      this.strategy.writeCache();

      // 如果需要再次编译,则再调用 `this.buildDeps()` 进行编译
      if (this.buildDepsAgain) {
        logger.info('[MFSU] buildDepsAgain');
        this.buildDepsAgain = false;
        this.buildDeps().catch((e: Error) => {
          printHelp.runtime(e);
        });
      }
    } catch (e) {
      this.lastBuildError = e;
      throw e;
    }
  }
}

从上面代码可以看出,实现依赖构建核心方法是 depBuilder.build,下面来看一下整体流程:

ts 复制代码
// /packages/mfsu/src/depBuilder/depBuilder.ts

export class DepBuilder {
  public opts: IOpts;
  public completeFns: Function[] = [];
  public isBuilding = false;

  constructor(opts: IOpts) {
    this.opts = opts;
  }

  // 此处省略一些代码

  async build(opts: { deps: Dep[] }) {
    this.isBuilding = true;

    // 构造 `onBuildComplete` 事件回调,在依赖构建完成后调用
    // 该方法会通知 devServer 响应 MF 静态资源请求
    const onBuildComplete = () => {
      this.isBuilding = false;
      this.completeFns.forEach((fn) => fn());
      this.completeFns = [];
    };

    try {
      // 写入依赖构建所需的文件
      await this.writeMFFiles({ deps: opts.deps });
      const newOpts = {
        ...opts,
        onBuildComplete,
      };
      if (this.opts.mfsu.opts.buildDepWithESBuild) {
        // 如配置了 `buildDepWithESBuild` 则调用 ESBuild 进行编译
        await this.buildWithESBuild(newOpts);
      } else {
        // 默认用 Webpack 进行编译
        await this.buildWithWebpack(newOpts);
      }
    } catch (e) {
      onBuildComplete();
      throw e;
    }
  }
}

writeMFFiles 方法会将收集到的依赖写入 tmpBase 目录,用于后续依赖构建。先过一遍整体流程:

ts 复制代码
// /packages/mfsu/src/depBuilder/depBuilder.ts

export class DepBuilder {
  // 此处省略一些代码

  async writeMFFiles(opts: { deps: Dep[] }) {
    const tmpBase = this.opts.mfsu.opts.tmpBase!;
    fsExtra.mkdirpSync(tmpBase);

    // 将收集到的依赖写入 tmpBase 目录,用于后续依赖构建
    for (const dep of opts.deps) {
      const content = await dep.buildExposeContent();
      writeFileSync(join(tmpBase, dep.filePath), content, 'utf-8');
    }

    // 这里是用于依赖构建的编译入口
    writeFileSync(
      join(tmpBase, 'index.js'),
      // https://webpack.js.org/concepts/module-federation/#infer-publicpath-from-script
      `__webpack_public_path__ = document.currentScript.src + '/../';`,
      'utf-8',
    );
  }
}

这里以 Webpack 为例,看一下 buildWithWebpack 构建流程:

ts 复制代码
// /packages/mfsu/src/depBuilder/depBuilder.ts

export class DepBuilder {
  // 此处省略一些代码

  async buildWithWebpack(opts: { onBuildComplete: Function; deps: Dep[] }) {
    // 从 `getWebpackConfig` 方法获取用于依赖构建的 Webpack 配置
    const config = this.getWebpackConfig({ deps: opts.deps });
    return new Promise((resolve, reject) => {
      // 创建一个新的 compiler 实例用于依赖构建
      const compiler = this.opts.mfsu.opts.implementor(config);
      // 启动构建
      compiler.run((err, stats) => {
        // 构建完成触发 `onBuildComplete` 构造
        opts.onBuildComplete();
        if (err || stats?.hasErrors()) {
          if (err) {
            reject(err);
          }
          if (stats) {
            const errorMsg = stats.toString('errors-only');
            // console.error(errorMsg);
            reject(new Error(errorMsg));
          }
        } else {
          resolve(stats);
        }
        // Webpack5 编译结束需要手动调用 `compiler.close()` 否则会继续监听文件编译
        compiler.close(() => {});
      });
    });
  }
}

上面还涉及到 getWebpackConfig 方法,简单过一下:

ts 复制代码
// /packages/mfsu/src/depBuilder/depBuilder.ts

export class DepBuilder {
  // 此处省略一些代码

  getWebpackConfig(opts: { deps: Dep[] }) {
    const mfName = this.opts.mfsu.opts.mfName!;
    // 获取 `depConfig` 配置项
    const depConfig = lodash.cloneDeep(this.opts.mfsu.depConfig!);

    // 添加依赖构建的编译入口
    // 注意这里用了 Infer publicPath from script 特性
    // entry 配置需要与 `ModuleFederationPlugin` 的 `name` 配置保持一致
    depConfig.entry = {
      [mfName]: join(this.opts.mfsu.opts.tmpBase!, 'index.js'),
    };
    // 打包输出路径
    depConfig.output!.path = this.opts.mfsu.opts.tmpBase!;
    // 关闭 devtool 配置
    depConfig.devtool = false;
    // 关闭库模式
    // library 会影响 external 的语法,导致报错
    // ref: https://github.com/umijs/plugins/blob/6d3fc2d/packages/plugin-qiankun/src/slave/index.ts#L83
    if (depConfig.output?.library) delete depConfig.output.library;
    if (depConfig.output?.libraryTarget) delete depConfig.output.libraryTarget;

    // 将所有依赖都打包到 vendor 里面
    depConfig.optimization ||= {};
    depConfig.optimization.splitChunks = {
      chunks: (chunk) => {
        // mf 插件中的 chunk 的加载并不感知到 mfsu 做了 chunk 的合并, 所以还是用原来的 chunk 名去加载
        // 这样就会造成 chunk 加载不到的问题; 因此将 mf shared 相关的 chunk 不进行合并
        const hasShared = chunk.getModules().some((m) => {
          return (
            m.type === 'consume-shared-module' ||
            m.type === 'provide-module' ||
            m.type === 'provide-shared-module'
          );
        });
        return !hasShared;
      },
      maxInitialRequests: Infinity,
      minSize: 0,
      cacheGroups: {
        vendor: {
          test: /.+/,
          name(_module: any, _chunks: any, cacheGroupKey: string) {
            return `${MF_DEP_PREFIX}___${cacheGroupKey}`;
          },
        },
      },
    };

    depConfig.plugins = depConfig.plugins || [];
    // `DepChunkIdPrefixPlugin` 给 chunkId 添加 `MF_DEP_PREFIX` 前缀
    depConfig.plugins.push(new DepChunkIdPrefixPlugin());
    // `StripSourceMapUrlPlugin` 用来给产物 JS 代码去掉 `sourceMappingURL`
    depConfig.plugins.push(
      new StripSourceMapUrlPlugin({
        webpack: this.opts.mfsu.opts.implementor,
      }),
    );
    // 应用 Webpack 内置 `ProgressPlugin`,实现 MFSU 编译进度回调
    depConfig.plugins.push(
      new this.opts.mfsu.opts.implementor.ProgressPlugin((percent, msg) => {
        this.opts.mfsu.onProgress({ percent, status: msg });
      }),
    );
    // 设置 `ModuleFederationPlugin` 插件的 `exposes` 选项
    const exposes = opts.deps.reduce<Record<string, string>>((memo, dep) => {
      memo[`./${dep.file}`] = join(this.opts.mfsu.opts.tmpBase!, dep.filePath);
      return memo;
    }, {});
    // 注意这里创建了一个新的 `ModuleFederationPlugin` 配置实例
    // 用于暴露依赖构建产物给业务工程消费
    depConfig.plugins.push(
      new this.opts.mfsu.opts.implementor.container.ModuleFederationPlugin({
        library: {
          type: 'global',
          name: mfName,
        },
        name: mfName,
        filename: REMOTE_FILE_FULL,
        exposes,
        shared: this.opts.mfsu.opts.shared || {},
      }),
    );
    return depConfig;
  }
}

以上就是 MFSU 整体流程,相信大家应该都有整体印象。如果对 MFSU 感兴趣的同学,可以进一步深入阅读源码。不过个人吐槽一句,跟其他模块相比,MFSU 整体代码质量不是很高,可以了解背后的原理,顺便学一些 Webpack 高级技巧。

参考

umijs.org/docs/guides...

umijs.org/blog/mfsu-i...

相关推荐
向前看-26 分钟前
验证码机制
前端·后端
燃先生._.1 小时前
Day-03 Vue(生命周期、生命周期钩子八个函数、工程化开发和脚手架、组件化开发、根组件、局部注册和全局注册的步骤)
前端·javascript·vue.js
高山我梦口香糖2 小时前
[react]searchParams转普通对象
开发语言·前端·javascript
m0_748235242 小时前
前端实现获取后端返回的文件流并下载
前端·状态模式
m0_748240253 小时前
前端如何检测用户登录状态是否过期
前端
black^sugar3 小时前
纯前端实现更新检测
开发语言·前端·javascript
寻找沙漠的人4 小时前
前端知识补充—CSS
前端·css
GISer_Jing4 小时前
2025前端面试热门题目——计算机网络篇
前端·计算机网络·面试
m0_748245524 小时前
吉利前端、AI面试
前端·面试·职场和发展