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

NextJS Module Federation:远程页面getServerSideProps无法执行求助

问题描述

基于Next.js v13搭建了宿主(Host)和远程(Remote)应用,远程应用暴露所有页面,挂载到宿主后接管路由。当前核心问题是:远程页面的getServerSideProps无法在宿主的[...slug].js路由中执行,这卡住了POC进度。

之前尝试在宿主页面的getServerSideProps中调用matchFederatedPage('/path'),抛出错误:

unhandledRejection: TypeError: Cannot read property 'asyncContainer' of undefined

需要解决的需求:在宿主的通配符路由中加载对应远程模块,同时执行该远程页面的getServerSideProps,并将获取到的props传递给挂载的远程组件。


现有代码

./shared.js

// ./shared.js
const React = require('react');
const createMatcher = require('feather-route-matcher');
const { injectScript } = require('@module-federation/nextjs-mf/utils');
import dynamic from 'next/dynamic';

const remoteVars = process.env.REMOTES || {};
export const remotes = Object.entries(remoteVars).reduce((acc, item) => {
  const [key, value] = item;
  if (typeof value !== 'string') {
    acc[key] = {
      global: value
    };
    return acc;
  }
  const [global, url] = value.split('@');

  acc[key] = {
    url,
    global
  };
  return acc;
}, {});

async function matchFederatedPage(path) {
  // console.log(remotes, 'shared');
  console.log(remoteVars, 'remoteVars');
  const maps = await Promise.all(
    Object.keys(remotes).map(async remote => {
      const foundContainer = injectScript(remote);
      const container = await foundContainer;

      return container
        .get('./pages-map')
        .then(factory => ({ remote, config: factory().default }))
        .catch(() => null);
    })
  );

  const config = {};

  for (const map of maps) {
    if (!map) continue;

    for (let [path, mod] of Object.entries(map.config)) {
      config[path] = {
        remote: map.remote,
        module: mod
      };
    }
  }

  console.log(config);
  const matcher = createMatcher.default(config);
  return matcher(path);
}

module.exports = {
  matchFederatedPage,
  createFederatedCatchAll() {
    const FederatedCatchAll = initialProps => {
      const [lazyProps, setProps] = React.useState({});

      const {
        FederatedPage,
        render404,
        renderError,
        needsReload,
        role,
        ...props
      } = {
        ...lazyProps,
        ...initialProps
      };
      console.log(props);


      React.useEffect(() => {
        if (needsReload) {
          const runUnderlayingGIP = async () => {
            const federatedProps = await FederatedCatchAll.getInitialProps(
              props
            );
            setProps(federatedProps);
          };
          runUnderlayingGIP();
        }
      }, []);

      if (render404) {
        // TODO: Render 404 page
        return React.createElement('h1', {}, '404 Not Found');
      }
      if (renderError) {
        // TODO: Render error page
        return React.createElement('h1', {}, 'Oops, something went wrong.');
      }

      if (FederatedPage) {
        return React.createElement(FederatedPage, props);
      }

      return null;
    };

    FederatedCatchAll.getInitialProps = async ctx => {
      // Bot marks "req, res, AppTree" as unused but those are vital to not get circular-dependency error
      const { err, req, res, AppTree, ...props } = ctx;
      if (err) {
        // TODO: Run getInitialProps for error page
        return { renderError: true, ...props };
      }

      // Custom
      let role;

      //
      if (!process.browser) {
        role = getCookie('token', req?.headers?.cookie);

        return { needsReload: true, ...props, role, remotes };
      } else {
        role = cookieCutter.get('token');
      }

      console.log('in browser', ctx.asPath);
      const matchedPage = await matchFederatedPage(ctx.asPath);

      try {
        console.log('matchedPage', matchedPage);
        const remote = matchedPage?.value?.remote;
        const mod = matchedPage?.value?.module;

        if (!remote || !mod) {
          // TODO: Run getInitialProps for 404 page
          return { render404: true, ...props };
        }

        console.log('loading exposed module', mod, 'from remote', remote);
        const container = await injectScript(remote);
        const FederatedPage = await container
          .get(mod)
          .then(factory => factory().default);
        console.log('FederatedPage', FederatedPage);
        if (!FederatedPage) {
          // TODO: Run getInitialProps for 404 page
          return { render404: true, ...props };
        }

        const modifiedContext = {
          ...ctx,
          query: matchedPage.params
        };
        const federatedPageProps =
          (await FederatedPage.getInitialProps?.(modifiedContext)) || {};
        return { ...federatedPageProps, FederatedPage, role, matchedPage };
      } catch (err) {
        console.log('err', err);
        // TODO: Run getInitialProps for error page
        return { renderError: true, ...props };
      }
    };

    return FederatedCatchAll;
  }
};

pages/[...slug].js

// pages/[...slug].js

import { createFederatedCatchAll } from '../shared.js';

export default createFederatedCatchAll();

解决方案

核心问题分析

报错Cannot read property 'asyncContainer' of undefined是因为服务器端调用injectScript时,模块联邦的容器初始化逻辑未适配Node.js环境;且原代码将getInitialProps的服务器端逻辑直接返回needsReload,把远程组件和getServerSideProps的执行丢给客户端,导致服务器端无法触发远程页面的服务端数据获取。

具体修改步骤

1. 修复matchFederatedPage的服务器端兼容性

修改shared.js中的matchFederatedPage,在服务器端手动加载远程容器:

async function matchFederatedPage(path) {
  const maps = await Promise.all(
    Object.keys(remotes).map(async remoteKey => {
      const remoteConfig = remotes[remoteKey];
      let container;

      if (!process.browser) {
        // 服务器端:手动初始化模块联邦容器
        const { initSharing } = await import('@module-federation/nextjs-mf/utils');
        await initSharing();
        // 动态引入远程entry文件
        await import(/* webpackIgnore: true */ remoteConfig.url);
        container = window[remoteConfig.global];
        await container.init(__webpack_share_scopes__.default);
      } else {
        // 客户端:保留原injectScript逻辑
        container = await injectScript(remoteKey);
      }

      return container
        .get('./pages-map')
        .then(factory => ({ remote: remoteKey, config: factory().default }))
        .catch(() => null);
    })
  );

  const config = {};
  for (const map of maps) {
    if (!map) continue;
    for (let [pagePath, mod] of Object.entries(map.config)) {
      config[pagePath] = {
        remote: map.remote,
        module: mod
      };
    }
  }

  const matcher = createMatcher.default(config);
  return matcher(path);
}

2. 重构宿主路由的服务端逻辑

放弃原createFederatedCatchAll中的getInitialProps,改用getServerSideProps直接在服务器端完成远程组件加载和数据获取:

修改pages/[...slug].js:

// pages/[...slug].js
import React from 'react';
import { matchFederatedPage, remotes } from '../shared.js';
import { injectScript } from '@module-federation/nextjs-mf/utils';

// 服务端获取远程组件和数据
export async function getServerSideProps(ctx) {
  const { asPath } = ctx;
  let role;

  // 获取用户角色(保留原逻辑)
  if (!process.browser) {
    role = getCookie('token', ctx.req?.headers?.cookie);
  } else {
    role = cookieCutter.get('token');
  }

  try {
    const matchedPage = await matchFederatedPage(asPath);
    if (!matchedPage?.value) {
      return { props: { render404: true } };
    }

    const { remote, module: mod } = matchedPage.value;
    const remoteConfig = remotes[remote];
    let container;

    if (!process.browser) {
      // 服务器端初始化容器
      const { initSharing } = await import('@module-federation/nextjs-mf/utils');
      await initSharing();
      await import(/* webpackIgnore: true */ remoteConfig.url);
      container = window[remoteConfig.global];
      await container.init(__webpack_share_scopes__.default);
    } else {
      container = await injectScript(remote);
    }

    // 获取远程页面组件
    const FederatedPage = await container.get(mod).then(factory => factory().default);
    if (!FederatedPage) {
      return { props: { render404: true } };
    }

    // 执行远程页面的getServerSideProps
    const modifiedContext = { ...ctx, query: matchedPage.params };
    const federatedProps = await FederatedPage.getServerSideProps?.(modifiedContext) || {};

    return {
      props: {
        ...federatedProps,
        FederatedPage,
        role,
        matchedPage
      }
    };
  } catch (err) {
    console.error('加载远程页面失败:', err);
    return { props: { renderError: true } };
  }
}

// 渲染组件
export default function FederatedCatchAll({ FederatedPage, render404, renderError, ...props }) {
  if (render404) {
    return <h1>404 Not Found</h1>;
  }
  if (renderError) {
    return <h1>Oops, something went wrong.</h1>;
  }
  if (FederatedPage) {
    return <FederatedPage {...props} />;
  }
  return null;
}

// 辅助函数:获取cookie
function getCookie(name, cookieStr) {
  if (!cookieStr) return null;
  const match = cookieStr.match(new RegExp(`(^| )${name}=([^;]+)`));
  return match ? match[2] : null;
}

3. 远程应用配置注意事项

  • 确保远程应用的next.config.js中正确暴露页面模块,模块需同时导出默认组件和getServerSideProps
  • 确保pages-map文件正确映射路由到对应的模块路径

关键说明

  • 服务器端通过手动初始化模块联邦容器,解决了injectScript在Node.js环境的兼容性问题
  • 将远程页面的getServerSideProps直接在宿主的getServerSideProps中执行,保证数据在服务端获取后传递给客户端
  • 客户端逻辑保留原injectScript,确保浏览器端正常加载远程组件

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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.26 04:39:52