MFE微前端高级版:Angular + Module Federation + webpack + 路由(Route way)完整示例

在查看这篇文章之前,可以先简单预览一下这篇基础版的博客:

MFE微前端基础版:Angular + Module Federation + webpack + 路由(Route way)完整示例 -CSDN博客

这篇Angular + Module Federation 高级路由配置详解包括嵌套路由、路由守卫、懒加载策略和动态路由等高级功能。

1. 主应用 (Shell) 高级路由配置

shell/src/app/app-routing.module.ts
javascript 复制代码
import { NgModule } from '@angular/core';
import { RouterModule, Routes, UrlSegment } from '@angular/router';
import { AuthGuard } from './core/guards/auth.guard';
import { MfePreloadStrategy } from './core/strategies/mfe-preload.strategy';

// 动态路由匹配函数 - 用于识别微前端路由
export function mfe1Matcher(url: UrlSegment[]) {
  return url.length > 0 && url[0].path.startsWith('mfe1-') ? 
    { consumed: [url[0]] } : null;
}

export function mfe2Matcher(url: UrlSegment[]) {
  return url.length > 0 && url[0].path.startsWith('mfe2-') ? 
    { consumed: [url[0]] } : null;
}

const routes: Routes = [
  {
    path: '',
    pathMatch: 'full',
    redirectTo: 'dashboard'
  },
  {
    path: 'dashboard',
    loadChildren: () => import('./dashboard/dashboard.module').then(m => m.DashboardModule),
    canActivate: [AuthGuard]
  },
  // 标准微前端路由配置
  {
    path: 'mfe1',
    loadChildren: () => import('mfe1/Module').then(m => m.Mfe1Module),
    data: { preload: true, mfe: 'mfe1' }
  },
  {
    path: 'mfe2',
    loadChildren: () => import('mfe2/Module').then(m => m.Mfe2Module),
    canLoad: [AuthGuard],
    data: { mfe: 'mfe2' }
  },
  // 动态微前端路由配置 - 使用自定义URL匹配器
  {
    matcher: mfe1Matcher,
    loadChildren: () => import('mfe1/DynamicModule').then(m => m.DynamicModule),
    data: { dynamic: true }
  },
  {
    matcher: mfe2Matcher,
    loadChildren: () => import('mfe2/DynamicModule').then(m => m.DynamicModule),
    data: { dynamic: true }
  },
  // 通配符路由 - 捕获所有未匹配的路由
  {
    path: '**',
    loadChildren: () => import('./not-found/not-found.module').then(m => m.NotFoundModule)
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes, {
    preloadingStrategy: MfePreloadStrategy, // 自定义预加载策略
    paramsInheritanceStrategy: 'always',    // 始终继承路由参数
    enableTracing: false                    // 生产环境应设为false
  })],
  exports: [RouterModule],
  providers: [MfePreloadStrategy]
})
export class AppRoutingModule { }

2. 自定义预加载策略

shell/src/app/core/strategies/mfe-preload.strategy.ts
javascript 复制代码
import { PreloadingStrategy, Route } from '@angular/router';
import { Observable, of } from 'rxjs';
import { MfeLoaderService } from '../services/mfe-loader.service';

@Injectable({ providedIn: 'root' })
export class MfePreloadStrategy implements PreloadingStrategy {
  constructor(private mfeLoader: MfeLoaderService) {}

  preload(route: Route, load: () => Observable<any>): Observable<any> {
    // 根据路由数据决定是否预加载
    if (route.data?.['preload'] && route.data?.['mfe']) {
      // 预加载微前端应用
      this.mfeLoader.preloadMfe(route.data['mfe']);
      return load();
    }
    return of(null);
  }
}

3. 微前端加载服务

shell/src/app/core/services/mfe-loader.service.ts
javascript 复制代码
import { Injectable } from '@angular/core';
import { LoadRemoteModuleOptions } from '@angular-architects/module-federation';

@Injectable({ providedIn: 'root' })
export class MfeLoaderService {
  private loadedMfes = new Set<string>();

  preloadMfe(mfeName: string): void {
    if (this.loadedMfes.has(mfeName)) return;

    const options: LoadRemoteModuleOptions = {
      type: 'module',
      exposedModule: './Module'
    };

    switch (mfeName) {
      case 'mfe1':
        options.remoteEntry = 'http://localhost:4201/remoteEntry.js';
        options.remoteName = 'mfe1';
        break;
      case 'mfe2':
        options.remoteEntry = 'http://localhost:4202/remoteEntry.js';
        options.remoteName = 'mfe2';
        break;
    }

    import('@angular-architects/module-federation').then(m => {
      m.loadRemoteModule(options).then(() => {
        this.loadedMfes.add(mfeName);
        console.log(`${mfeName} preloaded`);
      });
    });
  }
}

4. 微前端应用 (MFE1) 高级路由配置

mfe1/src/app/mfe1/mfe1.module.ts
javascript 复制代码
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { Mfe1HomeComponent } from './components/home/home.component';
import { Mfe1DetailComponent } from './components/detail/detail.component';
import { Mfe1Guard } from './guards/mfe1.guard';

const routes: Routes = [
  {
    path: '',
    component: Mfe1HomeComponent,
    children: [
      {
        path: 'detail/:id',
        component: Mfe1DetailComponent,
        data: {
          breadcrumb: 'Detail'
        }
      },
      {
        path: 'admin',
        loadChildren: () => import('./admin/admin.module').then(m => m.AdminModule),
        canLoad: [Mfe1Guard]
      },
      {
        path: 'lazy',
        loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule)
      }
    ]
  },
  // 动态模块暴露的路由
  {
    path: 'dynamic',
    loadChildren: () => import('./dynamic/dynamic.module').then(m => m.DynamicModule)
  }
];

@NgModule({
  imports: [RouterModule.forChild(routes)],
  exports: [RouterModule]
})
export class Mfe1RoutingModule { }

@NgModule({
  imports: [
    Mfe1RoutingModule,
    // 其他模块...
  ],
  declarations: [Mfe1HomeComponent, Mfe1DetailComponent]
})
export class Mfe1Module { }

5. 动态路由注册 (运行时路由)

shell/src/app/core/services/dynamic-routes.service.ts
javascript 复制代码
import { Injectable } from '@angular/core';
import { Router, Routes } from '@angular/router';

@Injectable({ providedIn: 'root' })
export class DynamicRoutesService {
  private dynamicRoutes: Routes = [];

  constructor(private router: Router) {}

  registerRoutes(newRoutes: Routes): void {
    // 合并新路由
    this.dynamicRoutes = [...this.dynamicRoutes, ...newRoutes];
    
    // 重置路由配置
    this.router.resetConfig([
      ...this.router.config, 
      ...this.dynamicRoutes
    ]);
  }

  unregisterRoutes(routesToRemove: Routes): void {
    this.dynamicRoutes = this.dynamicRoutes.filter(
      route => !routesToRemove.includes(route)
    );
    this.router.resetConfig([
      ...this.router.config.filter(
        route => !routesToRemove.includes(route)
      ),
      ...this.dynamicRoutes
    ]);
  }
}

6. 路由同步服务 (跨微前端路由状态管理)

shell/src/app/core/services/route-sync.service.ts
javascript 复制代码
import { Injectable } from '@angular/core';
import { Router, NavigationEnd } from '@angular/router';
import { filter } from 'rxjs/operators';

@Injectable({ providedIn: 'root' })
export class RouteSyncService {
  private currentRoute = '';

  constructor(private router: Router) {
    this.router.events.pipe(
      filter(event => event instanceof NavigationEnd)
    ).subscribe((event: NavigationEnd) => {
      this.currentRoute = event.url;
      // 可以在这里广播路由变化给所有微前端
      this.broadcastRouteChange(event.url);
    });
  }

  private broadcastRouteChange(url: string): void {
    // 使用自定义事件或状态管理库广播路由变化
    window.dispatchEvent(new CustomEvent('microfrontend:route-change', {
      detail: { url }
    }));
  }

  syncRoute(mfeName: string): void {
    // 微前端可以调用此方法来同步路由状态
    this.router.navigateByUrl(this.currentRoute);
  }
}

7. 微前端路由守卫示例

mfe1/src/app/guards/mfe1.guard.ts
javascript 复制代码
import { Injectable } from '@angular/core';
import { CanLoad, Route, UrlSegment, Router } from '@angular/router';
import { AuthService } from '../services/auth.service';

@Injectable({ providedIn: 'root' })
export class Mfe1Guard implements CanLoad {
  constructor(
    private authService: AuthService,
    private router: Router
  ) {}

  canLoad(route: Route, segments: UrlSegment[]): boolean {
    const requiredRole = route.data?.['requiredRole'];
    
    if (this.authService.hasRole(requiredRole)) {
      return true;
    }
    
    // 重定向到主应用的未授权页面
    this.router.navigate(['/unauthorized'], {
      queryParams: { returnUrl: segments.join('/') }
    });
    return false;
  }
}

8. 路由数据解析器

shell/src/app/core/resolvers/user.resolver.ts
javascript 复制代码
import { Injectable } from '@angular/core';
import { Resolve, ActivatedRouteSnapshot, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs';
import { UserService } from '../services/user.service';

@Injectable({ providedIn: 'root' })
export class UserResolver implements Resolve<any> {
  constructor(private userService: UserService) {}

  resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<any> {
    const userId = route.paramMap.get('id');
    return this.userService.getUser(userId);
  }
}

附录:Angular 路由的匹配规则和默认行为

1. 空路径路由的默认匹配
  • 示例配置

    javascript 复制代码
    const routes: Routes = [
      { path: '', component: HomeComponent }, // 空路径
      { path: 'login', component: LoginComponent }
    ];
  • 当访问根路径(如 http://localhost:4200)时,Angular 会优先匹配 path: '' 的路由规则。

  • 如果没有显式配置 redirectTo,但定义了空路径对应的组件(如 HomeComponent),则会直接渲染该组件。

2. 路由匹配的优先级规则
  • 规则 :Angular 按顺序匹配路由配置,第一个匹配的规则生效

  • 示例

    javascript 复制代码
    const routes: Routes = [
      { path: 'dashboard', component: DashboardComponent },
      { path: ':id', component: UserComponent } // 动态参数路由
    ];
  • 现象

    • 访问 /dashboard 会匹配第一个路由,渲染 DashboardComponent

    • 访问 /123 会匹配第二个路由,渲染 UserComponent

    • 即使没有 redirectTo,也能直接定位到组件。

3. 子路由的默认渲染
  • 示例配置

    javascript 复制代码
    const routes: Routes = [
      {
        path: 'admin',
        component: AdminComponent,
        children: [
          { path: '', component: AdminDashboardComponent }, // 子路由的空路径
          { path: 'users', component: UserListComponent }
        ]
      }
    ];
  • 现象

    • 访问 /admin 时,会渲染 AdminComponent 的模板,并在 <router-outlet> 中显示 AdminDashboardComponent(因为子路由的空路径匹配)。

    • 虽然未显式配置 redirectTo,但子路由的空路径规则会直接加载组件。

4. 路由通配符 (**) 的兜底行为
  • 示例配置

    javascript 复制代码
    const routes: Routes = [
      { path: 'home', component: HomeComponent },
      { path: '**', component: NotFoundComponent } // 通配符路由
    ];
  • 现象

    • 访问未定义的路径(如 /foo)时,会直接匹配 ** 规则,渲染 NotFoundComponent

    • 无需 redirectTo,通配符路由会直接显示组件。

5. 模块的默认路由
  • 懒加载模块的默认行为

    javascript 复制代码
    const routes: Routes = [
      { path: 'lazy', loadChildren: () => import('./lazy/lazy.module').then(m => m.LazyModule) }
    ];
    • 如果 LazyModule 内部的路由配置中有空路径规则:

      javascript 复制代码
      const routes: Routes = [
        { path: '', component: LazyHomeComponent } // 默认组件
      ];
    • 访问 /lazy 时,会直接渲染 LazyHomeComponent,而无需重定向。

6. 路由参数的隐式匹配
  • 动态参数路由

    javascript 复制代码
    const routes: Routes = [
      { path: 'product/:id', component: ProductDetailComponent }
    ];
  • 现象

    • 访问 /product/123 会直接渲染 ProductDetailComponent,无需重定向。
总结:
场景 原因
空路径 (path: '') 直接匹配空路径对应的组件。
子路由的空路径 父组件渲染后,子路由的空路径组件会自动显示。
动态参数路由 (:id) 路径匹配后直接渲染组件。
通配符路由 (**) 兜底路由直接显示组件。
懒加载模块的默认路由 模块内部的路由配置可能已经定义了空路径组件。
相关推荐
qq_2786672861 小时前
ros中相机话题在web页面上的显示,尝试js解析sensor_msgs/Image数据
前端·javascript·ros
烛阴1 小时前
JavaScript并发控制:从Promise到队列系统
前端·javascript
zhangxingchao1 小时前
关于《黑马鸿蒙5.0零基础入门》课程的总结
前端
zhangxingchao1 小时前
Flutter的Widget世界
前端
$程2 小时前
Vue3 项目国际化实践
前端·vue.js
nbsaas-boot2 小时前
Vue 项目中的组件职责划分评审与组件设计规范制定
前端·vue.js·设计规范
fanged2 小时前
Angular--Hello(TODO)
前端·javascript·angular.js
易鹤鹤.3 小时前
openLayers切换基于高德、天地图切换矢量、影像、地形图层
前端
可观测性用观测云3 小时前
从“烟囱式监控”到观测云平台:2025 亚马逊云科技峰会专访
前端
bluemliu3 小时前
django rest_framework 前端网页实现Token认证
前端·python·django