logologo
指南
实践
配置
插件
案例
博客
生态
Module Federation Examples
Practical Module Federation
Zephyr Cloud
Nx
简体中文
English
指南
实践
配置
插件
案例
博客
Module Federation Examples
Practical Module Federation
Zephyr Cloud
Nx
简体中文
English
logologo
概览

Bridge

Bridge 介绍

React Bridge

快速开始
导出应用
加载应用
加载模块
Vue Bridge

框架

框架概览

React

Basic CRA with Rsbuild
国际化 (i18n)

Modern.js

快速开始
动态加载生产者

Next.js

Basic Example
导入组件
路由和导入页面
使用 Express.js
预设

Angular

Angular CLI 设置
Micro-frontends with Angular
服务端渲染
使用 Service Workers
Authentication with Auth0
Authentication with Okta
拆分巨石应用
改造巨石应用
Edit this page on GitHub
Previous PageAngular CLI 设置
Next Page服务端渲染

#使用 Angular 和 Webpack Module Federation 的微前端

#概览

Module Federation 大大影响了微前端领域。当它与 Angular 结合使用时,为分布式前端架构提供了一个强大且可扩展的解决方案。本指南探讨了一个实际示例,详细介绍了如何在 Angular shell 和远程应用程序中配置 Webpack 的 ModuleFederationPlugin。

#前置要求

在开始设置之前,请确保满足以下先决条件:

  • Node.js 和 npm: 确保已安装 Node.js 和 npm。
  • Angular 和 Webpack 知识: 需要对 Angular 和 Webpack 有基本的了解。
  • Module Federation: 假设对 Module Federation 已经熟悉。

#准备步骤

#强制使用 Webpack 5

Angular CLI 项目通常预配置了 Webpack,但为了确保完全支持 Module Federation,需要选择使用 Webpack 5。

Yarn
Npm

Open your package.json and add a resolutions key to force the use of Webpack 5:

package.json
{
  "resolutions": {
    "webpack": "^5.0.0"
  }
}

#在 Angular CLI 中指定包管理器

angular.json
{
  "cli": {
    "packageManager": "yarn"
  }
}

#增加自定义 Webpack 配置

有几个选择来公开 Webpack 配置,例如使用 Ngx-build-plus 或 @angular-builders/custom-webpack。

在这个例子中,我们将使用后者。

首先,安装包:

yarn add @angular-builders/custom-webpack -D
# or
npm i -D @angular-builders/custom-webpack

然后,更新 “angular.json” 文件以使用此自定义构建器来执行构建和服务命令:

angular.json
{
  "projects": {
    "your-project-name": {
      "architect": {
        "build": {
          "builder": "@angular-builders/custom-webpack:browser",
          "options": {
            "customWebpackConfig": {
              "path": "webpack.config.ts"
            }
          }
        },
        "serve": {
          "builder": "@angular-builders/custom-webpack:dev-server"
        }
      }
    }
  }
}

自定义的 Webpack 配置将会与 Angular 的默认配置合并,这样只需要指定 Module Federation 所需的变更。

#设置消费者 Webpack 构建配置

#设置 uniqueName

Webpack 默认使用 package.json 中的名称。然而,为了避免冲突,尤其是在单仓库结构中,建议手动定义一个唯一的名称。

webpack.config.ts
config.output.uniqueName = 'shell';
TIP

注意:如果你没有使用单体仓库(monorepo),并且在你的 package.json 文件中已经有了唯一的名称,你可以跳过这一步。

#优化 runtime chunk

由于当前存在一个漏洞,将 runtimeChunk 优化设置为 false 是必要的;否则,模块联邦机制的设置将会失败。

webpack.config.ts
config.optimization.runtimeChunk = false;

#使用 Module Federation Plugin

在你的 webpack.config.ts 文件中,将 ModuleFederationPlugin 添加到插件数组中:

webpack.config.ts
import { CustomWebpackBrowserSchema, TargetOptions } from '@angular-builders/custom-webpack';
import { Configuration, container } from 'webpack';

export default (config: Configuration, options: CustomWebpackBrowserSchema, targetOptions: TargetOptions) => {
  // ... existing configuration
  config.plugins.push(
    new container.ModuleFederationPlugin({
      remotes: {
        'mf1': 'mf1@http://localhost:4300/mf1.js'
      },
      shared: {
        '@angular/animations': {singleton: true, strictVersion: true},
        '@angular/core': {singleton: true, strictVersion: true},
        // ... other shared modules
      }
    })
  );

  return config;
};

在这里,在 remotes 对象中,我们将远程模块名称映射到它们各自的位置。键(本例中的 'mf1')是用于在消费者消费者应用程序中导入模块的名称。值指定远程文件的位置,在这个例子中是 http://localhost:4300/mf1.js。

#配置生产者模块/应用

#设置 uniqueName 以及禁用 runtimeChunk

类似于消费者,定义一个唯一的输出名称并禁用 runtimeChunk 优化:

config.output.uniqueName = 'contact';
config.optimization.runtimeChunk = false;

#使用 Module Federation Plugin

使用 ModuleFederationPlugin,配置如下:

import { CustomWebpackBrowserSchema, TargetOptions } from '@angular-builders/custom-webpack';
import { Configuration, container } from 'webpack';
import * as path from 'path';

export default (config: Configuration, options: CustomWebpackBrowserSchema, targetOptions: TargetOptions) => {
  // ... existing configuration

  config.plugins.push(
    new container.ModuleFederationPlugin({
      filename: "mf1.js",
      name: "mf1",
      exposes: {
        './Contact': path.resolve(__dirname, './src/app/contact/contact.module.ts'),
        './Clock': path.resolve(__dirname, './src/app/clock/index.ts'),
      },
      shared: {
        '@angular/animations': {singleton: true, strictVersion: true},
        // ... other shared modules
      }
    })
  );

  return config;
};

在这里,filename 和 name 属性指定了 JavaScript 文件的名称以及模块容器在全局 window 对象中的命名空间。这些正是消费者消费者应用程序在加载远程模块时使用的确切值。

#提供模块

exposes 对象指明了要被导出的模块。在这个例子中:

  • ./Contact 导出了一个带有子路由的 Angular NgModule。
  • ./Clock 导出了一个用于运行时渲染的 Angular 组件。

#Angular 路由

#声明生产者模块类型

在你使用远程模块之前,你需要通知 TypeScript 它们的存在,因为它们将在运行时动态加载。

在你的路由模块旁边创建一个新的 TypeScript 定义文件 remote-modules.d.ts:

declare module 'mf1/Contact';
declare module 'mf1/Clock';

#在路由中懒加载远程模块

就像你对本地懒加载模块的操作一样,你现在可以将远程模块导入到你的 Angular 路由配置中。

请按以下方式修改你的路由配置:

const routes: Routes = [
  {
    path: '',
    loadChildren: () => HomeModule
  },
  {
    path: 'contact',
    loadChildren: () => import('mf1/Contact').then(m => m.ContactModule)
  },
  // ... other routes
];

#Dynamic Component Creation of Remote Modules

Creating components dynamically from remote modules offers a more advanced level of integration. This involves setting up a service and a directive to handle the dynamic rendering.

#远程模块加载服务

此服务负责动态加载远程模块并解析组件工厂。

@Injectable({
  providedIn: 'root'
})
export class RemoteModuleLoader {
  constructor(private _componentFactoryResolver: ComponentFactoryResolver) {}

  async loadRemoteModule(name: string) {
    const [scope, moduleName] = name.split('/');
    const moduleFactory = await window[scope].get('./' + moduleName);
    return moduleFactory();
  }

  getComponentFactory(component: Type<unknown>): ComponentFactory<unknown> {
    return this._componentFactoryResolver.resolveComponentFactory(component);
  }
}

#远程组件渲染器指令

这个结构型指令使用从远程模块加载服务获取的组件工厂,在它自己的视图容器中动态创建组件。

@Directive({
  selector: '[remoteComponentRenderer]'
})
export class RemoteComponentRenderer implements OnInit {
  @Input() set remoteComponentRenderer(componentName: string) { /* ... */ }
  @Input() set remoteComponentRendererModule(moduleName: RemoteModule) { /* ... */ }

  // ... other code

  private async renderComponent() {
    const module = await this.remoteModuleLoaderService.loadRemoteModule(this._moduleName);
    const componentFactory = this.remoteModuleLoaderService.getComponentFactory(module[this._componentName]);
    this.viewContainerRef.createComponent(componentFactory, undefined, this.injector);
  }
}

#视图使用方式

在 Angular 视图中,可以使用如下指令:

<ng-container *remoteComponentRenderer="'ClockComponent'; module:'mf1/Clock'"></ng-container>

#共享依赖

#共享依赖的重要性

在 Webpack 配置中的 shared 部分在定义消费者(shell)和远程模块共有的模块时起着关键作用。这样做可以显著减少打包大小,增强用户体验。

#处理版本不匹配

由于消费者(shell)和远程应用程序之间主要版本的不匹配,Webpack 可能会在运行时抛出错误。对于共享的 singletons 来说,保持一致的版本同步非常重要。

#语义化版本控制和灵活性

Webpack 在解析共享依赖时遵循语义化版本控制。建议在使用 ^ 或 >= 等操作符时允许一定版本的灵活性。这确保只加载必要的版本,最小化加载库的多个冲突版本的风险。

#总结

本指南通过使用 Webpack 的 Module Federation 介绍了在 Angular 应用程序中动态集成远程模块。具体来说,你已经学到:

  • 如何设置 Yarn 作为你的包管理器。
  • 为你的 Angular 构建自定义 Webpack 配置。
  • 在消费者(shell)和微前端应用中利用模块联邦。
  • 在 Angular 路由中懒加载远程模块。
  • 动态创建来自远程模块的组件。

对于生产就绪的设置,还需进行额外的步骤,这些将在未来的指南中介绍。如果你对这项技术有任何问题,欢迎通过我们的社交网络与我们联系。