返回

Angular 12 Host 应用无法渲染 Angular 15 MFE 嵌套组件的解决方案

javascript

Angular 12 Host 应用无法渲染 Angular 15 MFE 的嵌套组件问题排查及解决方案

遇到问题了?用 Angular 12 做的 Host 应用,加载 Angular 15 写的微前端(MFE)时,MFE 的主页面能显示,但里面的嵌套组件却渲染不出来? 别急,咱们一步步来解决。

一、 问题现象

Host 应用是 Angular 12.1.5, MFE 是 Angular 15.2.10。 MFE 能成功加载到 Host 应用,主页面的元素也能正常显示,但嵌套组件却不见踪影。 就好像<test-root>这个标签能识别,里面的Hello World !!!!!!!!没了。

二、 问题原因分析

这种情况,多半跟版本差异、模块联邦配置,或者组件的声明方式有关。可能的原因包括但不限于:

  1. Angular 版本差异导致的不兼容: Angular 12 和 15 跨度有点大, 某些 API 或者渲染机制可能有变化。
  2. 模块联邦 (Module Federation) 配置问题: webpack.config.js 里的共享库配置不当,可能导致组件无法正确加载。
  3. 组件未正确导出或声明: MFE 中的嵌套组件可能没有在其模块中正确声明或导出。
  4. Zone.js 相关问题: 不同版本的 Angular 可能使用了不同版本的 Zone.js,处理异步任务的方式可能不同。
  5. 构建配置差异: 开发环境和生产环境的构建配置有差别, 可能会导致仅在特定环境重现此问题。
  6. CSS 样式冲突或缺失: Host 和 MFE 样式可能会冲突,导致样式被覆盖,显示出现异常。

三、 解决方案

下面提供几种排查和解决问题的方案,可以逐一尝试。

1. 调整共享库的 requiredVersion

  • 原理: 严格的版本控制可能会阻止低版本的 Host 应用加载高版本的 MFE 的共享库。
  • 操作: 在 MFE 的 webpack.config.js 中,将 requiredVersion 设置为一个更宽松的范围, 或者直接移除requiredVersion,改成eager: true
  • 代码示例 (MFE 的 webpack.config.js):
 ```javascript
 shared: share({
   "@angular/core": {
     singleton: true,
     strictVersion: true,
     //requiredVersion: ">=1.0.0", // 移除此行, 或者用下面的范围
     requiredVersion: '^12.0.0 || ^15.0.0', // 或者允许这两个主版本
     eager: true, // 如果可行, 改为eager.
   },
   "@angular/common": {
     singleton: true,
     strictVersion: true,
     //requiredVersion: ">=1.0.0",// 移除此行
      requiredVersion: '^12.0.0 || ^15.0.0',
      eager: true,
   },
   "@angular/common/http": {
     singleton: true,
     strictVersion: true,
     //requiredVersion: ">=1.0.0",// 移除此行
     requiredVersion: '^12.0.0 || ^15.0.0',
     eager: true,
   },
   "@angular/router": {
     singleton: true,
     strictVersion: true,
     //requiredVersion: ">=1.0.0",// 移除此行
      requiredVersion: '^12.0.0 || ^15.0.0',
      eager: true,
   },
    ...sharedMappings.getDescriptors()
 })
 ```
  • 安全建议: 使用eager: true时,要保证你的 Host 应用知道并能处理所有可能的来自 MFE 的依赖。 这种方法可能会增大初始包的大小,但减少运行时依赖问题的风险。

2. 确保嵌套组件已正确声明

  • 原理: Angular 需要知道哪些组件属于哪个模块。 如果组件没有在模块的 declarations 数组中声明,就无法被渲染。
  • 操作: 检查 MFE 的 admin.module.ts(或者你暴露的模块),确保 TestComponent(或任何嵌套组件)已在 declarations 数组中。
  • 代码示例 (MFE 的 admin.module.ts):
 ```typescript
 import { NgModule } from '@angular/core';
 import { CommonModule } from '@angular/common';
 import { HomeComponent } from './home/home.component'; // 假设的 Home 组件路径
 import { TestComponent } from './test/test.component'; // 假设的 Test 组件路径
 import { AdminRoutingModule } from './admin-routing.module'; //你的路由配置

 @NgModule({
   declarations: [
     HomeComponent,
     TestComponent // 确保这里有 TestComponent
   ],
   imports: [
     CommonModule,
     AdminRoutingModule
   ],
   // 如果有需要外部使用的组件,在这里导出
 })
 export class AdminModule { }
 ```

3. 调整 MFE 的启动方式 (bootstrap)

  • 原理: 默认的启动方式可能在版本差异下出现问题,尝试更显式的启动方式.
  • 操作: 在MFE 的main.ts 文件中, 不直接使用platformBrowserDynamic().bootstrapModule(AppModule)。而是导出 bootstrap 函数。
  • 代码示例(MFE 的 main.ts):
 import { enableProdMode, NgZone } from '@angular/core';
 import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
 import { Router } from '@angular/router';
 import { AppModule } from './app/app.module';
 import { environment } from './environments/environment';
 import { singleSpaAngular, getSingleSpaExtraProviders } from 'single-spa-angular';
 
 if (environment.production) {
   enableProdMode();
 }
  
 const lifecycles = singleSpaAngular({
     bootstrapFunction: async (singleSpaProps) => {
        const ngZone =  new NgZone({ enableLongStackTrace: true }); // 或许你可以显式的创建一个新的NgZone实例.
         const moduleRef = await platformBrowserDynamic(getSingleSpaExtraProviders()).bootstrapModule(AppModule,{ngZone:ngZone});

       return moduleRef;
     },
     template: '<app-root />', // 或者你MFE根组件的selector
     Router,
     NgZone: NgZone, //将 NgZone 传入.
 });
   
 export const bootstrap = lifecycles.bootstrap;
 export const mount = lifecycles.mount;
 export const unmount = lifecycles.unmount;
  • 在你的MFE webpack config 中
  new ModuleFederationPlugin({
       library: { type: "var", name: "mfe1" }, //增加 library 属性, 其中mfe1 是你remote 的名字。
      //... 其他配置.
 }),

4. 检查 Zone.js 的兼容性

*  **原理:**   Angular 使用 Zone.js 来管理异步操作。不同版本 Angular 的 Zone.js 实现可能不完全兼容,有时会导致变更检测问题。
* **操作** : 尝试将 Host 和 MFE 的 Zone.js 版本统一。或者,在 Host 应用的 `polyfills.ts` 中显式导入特定版本的 Zone.js。
  ```typescript
  // polyfills.ts
  import 'zone.js/dist/zone'; // 或 'zone.js'; 较新版本的angular 使用 'zone.js';
  ```
  也可以尝试在 MFE 的`main.ts`文件手动创建一个新的 NgZone 实例,然后在启动 AppModule 的时候传入:
 ```typescript
  //main.ts MFE 中
   platformBrowserDynamic().bootstrapModule(AppModule, { ngZone: new NgZone({ enableLongStackTrace: true }) })
  .catch(err => console.error(err));
 ```
 * **安全建议:**  手动管理Zone.js需要对 Zone.js 有较深入的了解,否则容易引发其他问题,只在必要时这样做。

5. 使用 ngDoBootstrap

  • 原理 : 对于一些更复杂的场景,可以用ngDoBootstrap 来手动控制启动过程.
  • 操作:
    在你的 MFE 的模块中,实现 DoBootstrap 接口,并手动引导组件.
     import { ApplicationRef, DoBootstrap, NgModule } from '@angular/core';
      import { createCustomElement } from '@angular/elements';
      import { BrowserModule } from '@angular/platform-browser';
    
      import { AppComponent } from './app.component'; //导入根组件
    
       @NgModule({
         declarations: [AppComponent],
        imports: [BrowserModule],
          // 不需要 providers: [], 因为我们要手动操作
         entryComponents: [AppComponent] //非常重要
     })
       export class AppModule implements DoBootstrap {
    
        constructor() {}
    
        ngDoBootstrap(appRef: ApplicationRef) {
    
         const element = createCustomElement(AppComponent, { injector: appRef.injector });
         customElements.define('test-root', element); // 使用 createCustomElement, 把你的test-root 定义为一个custom element.
    
          // 注意: 不要在这里调用 appRef.bootstrap(AppComponent);
           // 因为你已经将组件定义成了 custom element
           // 在 Host 应用加载的时候,`<test-root>`会根据这个定义, 自动初始化.
    
       }
       }
    
  • 进阶: 这种方法比较适合复杂的控制场景,例如,你希望在 MFE 完全启动之前做一些事情,或者需要更精细地控制组件的创建。

6. 启用prodMode

  • 原理: prodMode 能优化代码, 可能在开发环境下正常, 但是生产环境失败。
  • 操作: Host/Shell 及Remote均进行启用prodMode

7. 查看构建日志

  • 原理: 在 Host 应用重新 build MFE,仔细查看有没有任何 warning 或者 error, 可能会有提示
  • 操作: 重新 ng build

通过上面这些步骤,基本可以解决由于版本差异及配置原因导致的问题。如果问题仍然存在, 就需要更多信息 (例如更完整的项目结构、错误截图等等), 来做进一步诊断。