宿主应用SSR模式下模块联邦引入的共享组件未渲染问题求助
问题描述
我在搭配使用模块联邦与SSR时遇到问题,附复现代码示例:
项目启动步骤
yarn install:allyarn shell:server:buildyarn shell:server:startyarn 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标签内容,你当前的远程模块加载逻辑完全依赖浏览器端的脚本加载机制,无法在服务端生效。按以下步骤修改即可:
- 新增服务端专属的远程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; }
- 改造原有加载逻辑,区分两端执行分支
// 改造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 ); }
- 额外配置校验
- 宿主服务端构建的模块联邦shared配置必须和客户端完全一致,避免共享模块版本不匹配导致init失败
- 远程应用需要同时输出兼容node环境的umd格式remoteEntry
- 远程组件依赖的所有三方包,要么在宿主服务端构建中存在,要么在模块联邦的shared配置中声明
内容的提问来源于stack exchange,提问作者Владислав Марченко
相关产品推荐
相关产品推荐

