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
相关产品推荐
相关产品推荐

