server-side-render-only 深度解析:纯服务端渲染下的 Module Federation 组件共享实战

原创2026-10-14 18:03:231,420 阅读
文章标签:示例工程微前端前端

server-side-render-only 深度解析:纯服务端渲染下的 Module Federation 组件共享实战

本指南以 module-federation-examples 仓库中的 server-side-render-only 示例 为蓝本,讲解在完全没有客户端渲染实现(no client side implementation)的前提下,如何通过 Module Federation 让两个独立部署的 Node 服务端(Host 与 Remote)共享同一个 React 组件,并实现"修改远端组件后,Host 无需重新构建即可同步生效"的热更新式协作。读完本文,你将掌握服务端模块联邦的核心 webpack 配置、动态导入规避启动崩溃的工程技巧,以及一套可复用的端到端验证方案。

示例整体架构:两个 Node 服务、一份共享组件

该示例由两个 Express 服务构成,两者都使用 react-dom/server 的 renderToString 在服务端把组件渲染为 HTML 字符串后直接返回,全程不加载任何客户端 bundle:

  • Host Server(hostServer):监听 http://localhost:3000/,负责消费远程组件;
  • Remote Server(remoteServer):监听 http://localhost:3001/,负责暴露 SharedComponent。

两个服务的 HTML 中会分别出现 Host Server 与 Remote Server 的 <h1> 标题,而 SharedComponent.js 渲染的 Shared Component2222 文本会同时出现在两个页面中——这正是 Host 成功在服务端拉取并渲染远端组件的最直观证明。

npm start
http://localhost:3000/ - Host Server
http://localhost:3001/ - Remote Server

一键启动:webpack 监听构建 + nodemon 热重启

示例根目录的 package.json 用 concurrently 把四条命令并行执行,构成完整开发闭环:

"scripts": {
  "start": "concurrently \"npm run buildHost\" \"npm run startHost\" \"npm run buildRemote\" \"npm run startRemote\"",
  "buildHost": "webpack --config=hostServer/webpack.config.js -w",
  "startHost": "nodemon hostServer/public/server/server.js",
  "buildRemote": "webpack --config=remoteServer/webpack.config.js -w",
  "startRemote": "nodemon remoteServer/public/server/server.js",
  "test:e2e": "pnpm exec playwright test"
}

其工作链路为:

  1. webpack ... -w 以 watch 模式把两个 server.js 入口分别编译到 public/server/ 目录;
  2. nodemon 监听编译产物并重启对应的 Node 进程;
  3. 由于编译产物被持续监听,任何源文件变更都会自动触发重新构建与进程重启。

值得注意的依赖细节:@module-federation/enhanced(示例锁定的版本为 2.0.1)提供服务端联邦插件,react 与 react-dom 使用 ^16.13.0,express 负责 HTTP 响应。示例在 start 前还会执行 prestart 钩子 node ../scripts/track-demo-run.js,用于统计演示运行情况,不影响功能本身。

Host 端配置:服务端如何"消费"远程组件

Host 的完整配置位于 hostServer/webpack.config.js,关键点如下:

new ModuleFederationPlugin({
  experiments: { asyncStartup: true },
  name: 'website1',
  shareStrategy: 'loaded-first',
  library: { type: 'commonjs-module' },
  filename: 'container.js',
  remotes: {
    website2: path.resolve(__dirname, '../remoteServer/public/server/container.js'),
  },
  //shared: ["react", "react-dom"],
})
  • experiments: { asyncStartup: true }:启用异步启动,允许联邦容器在运行时加载远程模块后再完成自身初始化,是服务端场景下避免启动阶段直接报错的关键开关;
  • library: { type: 'commonjs-module' }:声明联邦容器以 CommonJS 模块形式导出,使其可以被 Node 端 require/import 消费;
  • remotes 中 website2 直接指向 Remote 构建产物 ../remoteServer/public/server/container.js 的本地文件路径,而非网络 URL——这是纯服务端场景的常见做法,因为两个进程运行在同一台机器上;
  • shareStrategy: 'loaded-first':指定共享模块的加载优先级策略;
  • 注释掉的 shared: ["react", "react-dom"] 表明该示例刻意不共享 React 运行时,两个服务各自携带独立副本,从而将关注点完全收敛到"组件共享"本身。

与 Host 配套的 App.js 以常规方式引入远程组件:

import SharedComponent from 'website2/SharedComponent';

const App = () => (
  <div>
    <h1>Host Server</h1>
    <SharedComponent />
  </div>
);

关键工程细节:为何 Host 必须动态 import App.js

hostServer/server.js 中藏着一个重要的启动顺序问题,源码注释直接给出了解释:

app.get('/*', async (req, res, next) => {
  // Don't statically import App.js, otherwise the Host app crashes on startup
  // because there's no webpack module entry for "website2/SharedComponent" yet.
  // The MF plugin will populate it during runtime, so dynamically import App.js to give it time

  const App = (await import('./App')).default;
  const html = renderToString(<App />);
  res.send(html);
});

含义是:如果 Host 在进程启动时静态 import './App',由于此刻联邦插件还没有在运行时注入 website2/SharedComponent 这个模块条目,进程会直接崩溃。因此示例把 import 推迟到每次请求处理时动态执行,给模块联邦插件留出运行时填充远程模块的窗口。这一"请求时动态解析"的模式,是服务端模块联邦区别于浏览器端(后者通常依赖 <script> 注入时序)的核心差异点。

Remote 端配置:服务端如何"暴露"组件

Remote 的配置位于 remoteServer/webpack.config.js:

new ModuleFederationPlugin({
  experiments: { asyncStartup: true },
  name: 'website2',
  shareStrategy: 'loaded-first',
  library: { type: 'commonjs-module' },
  filename: 'container.js',

  exposes: {
    './SharedComponent': './remoteServer/SharedComponent',
  },
  //shared: ["react", "react-dom"],
})

与 Host 对称:name: 'website2' 定义了被引用方标识(Host 配置中的 website2 即指向它),exposes 把 SharedComponent.js 以 ./SharedComponent 为键暴露出去;filename: 'container.js' 是生成联邦容器的文件名,Host 的 remotes 路径正是解析到该产物。Remote 自身的 server.js 采用常规静态导入 import App from './App' 即可,因为 SharedComponent 就在它自己的模块作用域内,不存在远程解析问题。

"修改 Remote 组件、Host 无需重建"的原理与验证

README 的核心主张是:Changing "SharedComponent" will affect both servers, without rebuilding Host server(修改 SharedComponent 会同时影响两个服务,且无需重新构建 Host)。其成立依赖三重机制:

  1. Host 对 Remote 的引用是文件路径级的,每次请求时动态解析 website2/SharedComponent,实际读取的是 Remote 的构建产物;
  2. Remote 处于 webpack -w watch 模式,SharedComponent 变更会触发 Remote 侧重新构建;
  3. Host 的 nodemon 只需在 Remote 容器产物变化后重启,而 Host 自己的源码与构建产物并未改变——严格意义上不需要重新执行 Host 的构建步骤。

仓库内的端到端测试对这一点做了自动化实证:e2e/tests/checkServersideRenderOnlyApp.spec.ts 直接读写 remoteServer/SharedComponent.js 源文件:

  • beforeAll 把组件内容写回原始文本 Shared Component2222(对应 constants.ts 中 serverSideRenderOnlyApp.contents.originalContent);
  • 在 Update Shared component file 用例中,把文件改写为 Updated Shared conponent in test,随后 reloadWindow() 刷新页面,断言两个服务都能看到更新后的文本;
  • Check Shared component visibility in both apps after updating 用例进一步交叉验证:在 3000 端口更新后,切到 3001 端口刷新同样能看到新内容;
  • afterEach 与 afterAll 把文件恢复为原始内容,保证测试可重复执行。

测试中两个 app 的数据结构(appsData)分别覆盖 Host Server(3000)与 Remote Server(3001)两个端口,验证逻辑完全一致,从测试层面确认了"更新远端组件、两端同步生效"这一核心能力。

端到端测试的运行方式

示例自带 playwright.config.ts,可通过 pnpm exec playwright test(或脚本 test:e2e)运行:

  • webServer 字段以 pnpm start 启动整套服务,等待 3000 端口就绪,reuseExistingServer 在非 CI 环境下允许复用已启动的服务;
  • 测试配置为单 worker 串行执行(workers: 1、fullyParallel: false),这是因为测试会改写 SharedComponent 源文件,串行可避免并发互相污染;
  • 运行命令 pnpm start 会先把两个 webpack 服务端构建产物输出到各自 public/server/ 目录,再启动 Express 进程监听 3000/3001 端口。

关于 Playwright 基类(如 openLocalhost、reloadWindow、checkElementWithTextPresence 等)的约定与仓库级测试规范,可参考仓库根目录下的 playwright-e2e/README.md(该文档也是示例 README 中推荐阅读的进阶材料)。

小结:纯服务端模块联邦的适用边界

通过本示例可以提炼出一套"服务端-only"模块联邦的最小范式:Remote 以 CommonJS 联邦容器暴露组件,Host 通过本地文件路径引用该容器并借助 experiments.asyncStartup 在请求时动态解析远程模块,最后由 renderToString 直接输出 HTML。这种方案适合无需客户端交互、以服务端直出为主的场景(如 SEO 页面、邮件模板渲染等);由于示例刻意未配置 shared,React 运行时会分别打包进两个服务,若希望进一步复用依赖,可以参考仓库中其他 SSR 类示例(如 server-side-rendering、nextjs-ssr)中的共享配置写法。

登录后查看全文
module-federation-examples