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

宿主应用SSR模式下模块联邦引入的共享组件未渲染问题求助

问题描述

我在搭配使用模块联邦与SSR时遇到问题,附复现代码示例:

项目启动步骤

  • yarn install:all
  • yarn shell:server:build
  • yarn shell:server:start
  • yarn remote:start

我有两个独立应用:一个是搭载SSR、承载组件渲染逻辑的宿主应用,另一个是提供共享组件的远程应用。我已在宿主应用中配置ng-universal,期望页面加载时返回的响应包含完整的渲染代码,但目前仅返回宿主应用本身的代码。我使用以下代码实现从remoteEntry.js获取组件:

import { LoadRemoteModuleOptions } from '../interfaces/module-federation.interfaces';

type Scope = unknown;
type Factory = () => any;

type Container = {
    init(shareScope: Scope, initScope?: Scope): void;
    get(module: string, getScope?: Scope): Promise<Factory>;
};


const createContainer = (name: string): Container => {
  // @ts-ignore
  const container = window[name] as Container;
  return container
} ;

declare const __webpack_init_sharing__: (shareScope: string) => Promise<void>;
declare const __webpack_share_scopes__: { default: Scope, plugin: Scope };

const moduleMap: any = {};

export function loadRemoteEntry(remoteEntry: string): Promise<boolean> {
    return new Promise<any>((resolve, reject) => {
        if (moduleMap[remoteEntry]) {
            resolve(moduleMap[remoteEntry]);
            return;
        }

        const script = document.createElement('script');
        script.src = remoteEntry;

        script.onerror = reject;

        script.onload = () => {
            moduleMap[remoteEntry] = true;
            resolve(moduleMap[remoteEntry]); // window is the global namespace
        };

        document.body.appendChild(script);
    });
}

async function lookupExposedRemote<T>(
    remoteName: string,
    exposedModule: string
): Promise<T> {
  // Initializes the share scope. This fills it with known provided modules from this build and all remotes
  await __webpack_init_sharing__('default');
  // @ts-ignore
  // @ts-ignore
  const container = createContainer(remoteName);
  if (!container) {throw new Error('Container with script is not defined')}
  await container.init(__webpack_share_scopes__.default);
  const factory = await container.get(exposedModule);
  const Module = factory();
  return Module as T;
}

export async function loadRemoteModule(
    options: LoadRemoteModuleOptions
): Promise<any> {
    await loadRemoteEntry(options.remoteEntry);
    return await lookupExposedRemote<any>(
        options.remoteName,
        options.exposedModule
    );
}

我推测问题根源是服务端不存在window对象,但我已经使用domino做了适配,依旧无法解决该问题,恳请各位帮忙排查,万分感谢。


解决方案

你适配domino无效的核心原因是:domino仅模拟了DOM结构,不会执行动态插入的script标签内容,你当前的远程模块加载逻辑完全依赖浏览器端的脚本加载机制,无法在服务端生效。按以下步骤修改即可:

  1. 新增服务端专属的远程Entry加载逻辑
    服务端不要用动态插入script的方式拉取remoteEntry,改用node原生http模块拉取脚本内容,通过vm模块执行后挂载到global全局对象上,替换浏览器端的window挂载逻辑。
import { PLATFORM_ID, Inject } from '@angular/core';
import { isPlatformServer, isPlatformBrowser } from '@angular/common';
import * as vm from 'vm';
import * as http from 'http';

const moduleMap: any = {};

// 服务端加载remoteEntry实现
async function loadRemoteEntryServer(remoteEntry: string, remoteName: string): Promise<boolean> {
  if (moduleMap[remoteEntry]) return true;
  // 拉取远程脚本内容
  const content = await new Promise<string>((resolve, reject) => {
    http.get(remoteEntry, res => {
      let data = '';
      res.on('data', chunk => data += chunk);
      res.on('end', () => resolve(data));
      res.on('error', reject);
    });
  });
  // 执行脚本挂载容器到global
  const script = new vm.Script(content);
  const context = vm.createContext({ 
    global, require, module, exports, 
    __webpack_share_scopes__: __webpack_share_scopes__ 
  });
  script.runInContext(context);
  // @ts-ignore
  global[remoteName] = context[remoteName];
  moduleMap[remoteEntry] = true;
  return true;
}
  1. 改造原有加载逻辑,区分两端执行分支
// 改造createContainer,根据运行环境从不同全局对象取容器
const createContainer = (name: string, platformId: Object): Container => {
  if (isPlatformBrowser(platformId)) {
    // @ts-ignore
    return window[name] as Container;
  }
  // @ts-ignore
  return global[name] as Container;
};

// 改造lookupExposedRemote,适配环境判断
async function lookupExposedRemote<T>(
    remoteName: string,
    exposedModule: string,
    platformId: Object
): Promise<T> {
  await __webpack_init_sharing__('default');
  const container = createContainer(remoteName, platformId);
  if (!container) throw new Error('Container with script is not defined');
  await container.init(__webpack_share_scopes__.default);
  const factory = await container.get(exposedModule);
  return factory() as T;
}

// 改造对外暴露的loadRemoteModule,入参增加platformId
export async function loadRemoteModule(
    options: LoadRemoteModuleOptions,
    platformId: Object
): Promise<any> {
    if (isPlatformBrowser(platformId)) {
      await loadRemoteEntry(options.remoteEntry);
    } else if (isPlatformServer(platformId)) {
      await loadRemoteEntryServer(options.remoteEntry, options.remoteName);
    }
    return await lookupExposedRemote<any>(
        options.remoteName,
        options.exposedModule,
        platformId
    );
}
  1. 额外配置校验
  • 宿主服务端构建的模块联邦shared配置必须和客户端完全一致,避免共享模块版本不匹配导致init失败
  • 远程应用需要同时输出兼容node环境的umd格式remoteEntry
  • 远程组件依赖的所有三方包,要么在宿主服务端构建中存在,要么在模块联邦的shared配置中声明

内容的提问来源于stack exchange,提问作者Владислав Марченко

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.10.03 19:15:03