使用 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。
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 路由中懒加载远程模块。
- 动态创建来自远程模块的组件。
对于生产就绪的设置,还需进行额外的步骤,这些将在未来的指南中介绍。如果你对这项技术有任何问题,欢迎通过我们的社交网络与我们联系。