You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Fastify SSR中加载Module Federation远程模块并提取JS/CSS块

Webpack Module Federation + Fastify SSR 模块加载与资源注入问题

背景

正在为大型应用构建原型,测试Webpack Module Federation(WMF)结合Server-Side Rendering(SSR)的方案,现有两个应用:

App 1 – 远程应用(React + Webpack)

基础Button1组件:

const Button1 = () => {
  return (
    <button>
      Btn1 Remote
    </button>
  );
};

export default Button1;

Webpack配置:

const ModuleFederationPlugin = require('webpack/lib/container/ModuleFederationPlugin');
...
devServer: {
  port: 3001,
  headers: {
    'Access-Control-Allow-Origin': '*',
  },
},
...
plugins: [
  new ModuleFederationPlugin({
    name: 'remoteApp',
    filename: 'remoteEntry.js',
    exposes: {
      './Button1': './src/Button1',
      './Button2': './src/Button2',
      ...
    },
    shared: ['react', 'react-dom'],
  }),
];

App 2 – 宿主应用(Fastify Server,SSR)

运行在3000端口,使用react-dom/server实现基础服务端渲染:

import { renderToString } from 'react-dom/server';
import cors from '@fastify/cors';
import Fastify from 'fastify';

const fastify = Fastify({ logger: true });

const Document = () => (
  <html lang="ru">
    <head></head>
    <body>
      <div id="react-app">hello</div>
    </body>
  </html>
);

(async () => {
  await fastify.register(cors, {
    origin: '*',
    methods: ['GET', 'POST'],
  });

  fastify.get('/', async (request, reply) => {
    const markup = renderToString(<Document />);
    return reply
      .code(200)
      .headers({
        'content-type': 'text/html',
        'Cache-control': 'no-store',
      })
      .send(`<!DOCTYPE html>${markup}`);
  });

  fastify.listen({ port: 3000 }, err => {
    if (err) {
      fastify.log.error(err);
      process.exit(1);
    }
  });
})();

需求

在App 2(Fastify)的SSR过程中,需要:

  1. 加载App 1中暴露的任意模块(如Button1)
  2. 获取该模块对应的JS和CSS块
  3. 手动将这些块注入到SSR HTML中(避免浏览器先加载remoteEntry.js再请求资源,让浏览器初始加载时直接获取资源)

最终期望HTML示例:

<html lang="ru">
  <head>
    <link rel="stylesheet" href="/remoteapp/assets/chunk1.css" crossorigin="anonymous" />
    <link rel="stylesheet" href="/remoteapp/assets/chunk2.css" crossorigin="anonymous" />
  </head>
  <body>
    <div id="react-app">hello</div>
    <script src="/remoteapp/assets/chunk1.js" crossorigin="anonymous"></script>
    <script src="/remoteapp/assets/chunk2.js" crossorigin="anonymous"></script>
  </body>
</html>

⚠️ 操作必须在服务端SSR期间完成,而非客户端。

已尝试方案

使用@module-federation/enhanced/runtime,代码如下:

import { createInstance } from '@module-federation/enhanced/runtime';

(async () => {
  const mf = await createInstance({
    name: 'hostApp',
    remotes: [
      {
        name: 'remoteApp',
        entry: 'http://localhost:3001/remoteEntry.js',
      },
    ],
    shared: {
      react: { singleton: true },
      'react-dom': { singleton: true },
    },
  });

  const mod = await mf.loadRemote('remoteApp/Button1'); // 抛出错误
  console.log('Loaded mod:', mod);
})();

但持续出现运行时错误,无法正常工作。

问题

  1. 如何在Fastify服务端SSR期间正确加载远程模块(如Button1)?
  2. 若加载成功,如何提取该模块依赖的JS/CSS块,以便将其包含在SSR HTML中?

解决方案

1. 服务端正确加载远程模块

服务端加载MF远程模块需要解决Node环境下的模块解析和共享依赖的singleton处理,不建议直接使用客户端增强runtime,改用服务端适配方案:

步骤1:修改远程应用Webpack配置,适配服务端

在远程应用的Webpack配置中添加libraryTarget: 'commonjs2',同时优化共享依赖配置:

new ModuleFederationPlugin({
  name: 'remoteApp',
  filename: 'remoteEntry.js',
  exposes: {
    './Button1': './src/Button1',
    './Button2': './src/Button2',
  },
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^18.0.0',
      eager: true, // 服务端需提前加载共享依赖
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^18.0.0',
      eager: true,
    },
  },
  library: { type: 'commonjs2' }, // 新增,适配Node环境
});

步骤2:宿主服务端手动加载并解析远程模块

在Fastify服务中,通过fetch获取remoteEntry.js,用vm模块隔离执行环境,再初始化容器获取目标模块:

import { renderToString } from 'react-dom/server';
import cors from '@fastify/cors';
import Fastify from 'fastify';
import vm from 'vm';
import fetch from 'node-fetch';

const fastify = Fastify({ logger: true });

// 封装远程模块加载函数
async function loadRemoteModule(remoteUrl, modulePath) {
  // 1. 获取remoteEntry.js内容
  const remoteEntryRes = await fetch(`${remoteUrl}/remoteEntry.js`);
  const remoteEntryCode = await remoteEntryRes.text();

  // 2. 创建隔离上下文,模拟必要全局变量
  const context = vm.createContext({
    module: {},
    exports: {},
    require: require,
    __dirname: __dirname,
    __filename: __filename,
    window: {}, // 避免客户端代码报错
  });

  // 3. 执行remoteEntry代码
  vm.runInContext(remoteEntryCode, context);

  // 4. 初始化远程容器,注入共享依赖
  const container = context.module.exports;
  await container.init({
    shareScope: {
      react: {
        get: () => Promise.resolve({ default: require('react') }),
      },
      'react-dom': {
        get: () => Promise.resolve({ default: require('react-dom') }),
      },
    },
  });

  // 5. 获取并返回目标模块
  const factory = await container.get(modulePath);
  return factory();
}

const Document = ({ Button1 }) => (
  <html lang="ru">
    <head></head>
    <body>
      <div id="react-app">{Button1 ? <Button1 /> : 'hello'}</div>
    </body>
  </html>
);

(async () => {
  await fastify.register(cors, {
    origin: '*',
    methods: ['GET', 'POST'],
  });

  fastify.get('/', async (request, reply) => {
    try {
      // 加载远程Button1模块
      const Button1 = await loadRemoteModule('http://localhost:3001', './Button1');
      // 渲染包含远程组件的页面
      const markup = renderToString(<Document Button1={Button1.default} />);
      return reply
        .code(200)
        .headers({
          'content-type': 'text/html',
          'Cache-control': 'no-store',
        })
        .send(`<!DOCTYPE html>${markup}`);
    } catch (err) {
      fastify.log.error(err);
      return reply.code(500).send('Failed to load remote module');
    }
  });

  fastify.listen({ port: 3000 }, err => {
    if (err) {
      fastify.log.error(err);
      process.exit(1);
    }
  });
})();

2. 提取并注入JS/CSS块

需要让远程应用生成模块- chunk映射文件,在构建时输出每个暴露模块对应的资源信息:

步骤1:远程应用添加自定义插件生成chunk映射

在远程应用的Webpack配置中添加自定义插件,输出chunk-mapping.json:

class ChunkMappingPlugin {
  apply(compiler) {
    compiler.hooks.emit.tapAsync('ChunkMappingPlugin', (compilation, callback) => {
      const chunkMapping = {};
      // 获取MF插件的暴露模块配置
      const mfPlugin = compiler.options.plugins.find(p => p instanceof ModuleFederationPlugin);
      const exposes = mfPlugin?.options.exposes || {};

      Object.keys(exposes).forEach(exposedPath => {
        const originalPath = exposes[exposedPath];
        // 找到对应模块所属的chunk
        const module = compilation.modules.find(m => m.resource?.includes(originalPath));
        if (module) {
          const chunks = Array.from(module.chunks);
          chunkMapping[exposedPath] = {
            js: chunks.flatMap(chunk => chunk.files.filter(f => f.endsWith('.js'))).map(f => `/remoteapp/assets/${f}`),
            css: chunks.flatMap(chunk => chunk.files.filter(f => f.endsWith('.css'))).map(f => `/remoteapp/assets/${f}`),
          };
        }
      });

      // 将映射写入输出文件
      compilation.assets['chunk-mapping.json'] = {
        source: () => JSON.stringify(chunkMapping),
        size: () => Buffer.byteLength(JSON.stringify(chunkMapping)),
      };
      callback();
    });
  }
}

// 在plugins数组中添加该插件
plugins: [
  new ModuleFederationPlugin({ ... }),
  new ChunkMappingPlugin(),
];

步骤2:宿主服务端获取映射并注入资源到HTML

修改Fastify路由逻辑,先获取chunk映射,再生成资源标签注入页面:

fastify.get('/', async (request, reply) => {
  try {
    // 1. 获取chunk映射文件
    const chunkMapRes = await fetch('http://localhost:3001/chunk-mapping.json');
    const chunkMap = await chunkMapRes.json();
    const button1Chunks = chunkMap['./Button1'] || { js: [], css: [] };

    // 2. 加载远程Button1模块
    const Button1 = await loadRemoteModule('http://localhost:3001', './Button1');

    // 3. 生成资源标签
    const cssLinks = button1Chunks.css.map(href => `<link rel="stylesheet" href="${href}" crossorigin="anonymous" />`).join('\n');
    const jsScripts = button1Chunks.js.map(src => `<script src="${src}" crossorigin="anonymous"></script>`).join('\n');

    // 4. 渲染页面并注入资源
    const componentMarkup = renderToString(<Button1.default />);
    const fullMarkup = `
<!DOCTYPE html>
<html lang="ru">
  <head>
    ${cssLinks}
  </head>
  <body>
    <div id="react-app">${componentMarkup}</div>
    ${jsScripts}
  </body>
</html>
`;
    return reply
      .code(200)
      .headers({
        'content-type': 'text/html',
        'Cache-control': 'no-store',
      })
      .send(fullMarkup);
  } catch (err) {
    fastify.log.error(err);
    return reply.code(500).send('Failed to load remote module');
  }
});

注意事项

  • 确保共享依赖(react、react-dom)版本一致,启用singleton避免重复加载
  • 自定义ChunkMappingPlugin需根据项目实际模块路径规则调整匹配逻辑
  • 生产环境建议缓存chunk-mapping.json,减少远程请求次数

内容的提问来源于stack exchange,提问作者War han

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.12 14:52:04