如何在 Angular 中使用模块联合构建微前端
对前端 Web 应用程序的需求持续增长。作为消费者,我们希望我们的 Web 应用程序功能丰富且高性能。作为开发人员,我们担心如何在提供高质量功能和性能的同时牢记良好的开发实践和架构。
进入微前端架构。微前端以与微服务相同的概念建模,作为分解整体式前端的一种方式。您可以组合微型前端以形成功能齐全的 Web 应用程序。由于每个微型前端都可以独立开发和部署,因此您有一种强大的方法来扩展前端应用程序。
那么微前端架构是什么样的呢?假设您有一个电子商务网站,看起来和这个网站一样令人惊叹:
销售带有标题的气球、帐户和购物车链接以及要购买的气球图像的示例电子商务网站的图像
您可能有一个购物车、注册用户的帐户信息、过去的订单、付款方式等。您可以进一步将这些功能分类到域中,每个域都可以是一个单独的微前端,也称为 .微前端遥控器的集合位于另一个网站,即Web应用程序。remotehost
因此,使用微前端分解不同功能的电子商务网站可能如下图所示,其中购物车和帐户功能位于单页应用程序 (SPA) 中的单独路由中:
Image of the example balloon e-commerce site showing a breakdown of the views. The entire site is wrapped in a host named 'Shell', and micro-frontends for the 'Cart' and 'Account' links
你可能会说,“微前端听起来很酷,但管理不同的前端和跨微前端的编排状态听起来也很复杂。你是对的。微前端的概念已经存在了几年,推出自己的微前端实现、共享状态和支持它的工具是一项艰巨的任务。但是,微前端现在得到了 Webpack 5 和模块联合的良好支持。并非所有 Web 应用程序都需要微前端架构,但对于那些已经开始变得笨拙的大型、功能丰富的 Web 应用程序,我们的 Web 工具中对微前端的一流支持绝对是一个加分项。
这篇文章是系列文章的第一部分,我们将使用 Angular 和微型前端构建一个电子商务网站。我们将使用带有模块联合的 Webpack 5 来支持将微前端连接在一起。然后,我们将演示在不同前端之间共享经过身份验证的状态,并将其全部部署到免费的云托管提供商。在第一篇文章中,我们将探索一个初学者项目,并了解不同应用程序如何连接,使用Okta添加身份验证,并添加共享身份验证状态的布线。最后,您将拥有一个看起来像这样的应用程序:
先决条件
Node: 这个项目是使用Node v16.14和npm v8.5开发的
Angular CLI
使用Webpack 5和模块联合的微前端启动器
使用OpenID Connect添加身份验证
创建一个新的Angular应用程序
适用于您的Angular应用程序的模块联合
微前端状态管理
下一步
了解Angular、OpenID Connect、微前端等
使用Webpack 5和模块联合的微前端启动器
这个网络应用程序里有很多!我们将使用初学者代码来确保我们专注于特定于微前端的代码。如果您对使用起动器而不是从头开始感到沮丧,请不要担心。我将提供Angular CLI命令,以便在存储库的README.md上重新创建此初学者应用程序的结构,以便您拥有所有说明。
按照以下步骤克隆Angular Micro Frontend示例GitHub repo,并在您最喜欢的IDE中打开repo。
git clone https://github.com/oktadev/okta-angular-microfrontend-example.git
cd okta-angular-microfrontend-example
npm ci
让我们深入研究代码!?/p>
我们有一个Angular项目,在src/projects目录中有两个应用程序和一个库。这两个应用程序被命名为shell和mfe-basket,库被命名为shared。shell应用程序是微前端主机,mfe-basket是微前端远程应用程序。shared库包含我们想要在整个网站上共享的代码和应用程序状态。当您为该应用程序应用上面显示的相同类型的图表时,它看起来像这样:
在这个项目中,我们使用@angular-architects/module-federation依赖项来帮助封装配置Webpack和模块联合插件的一些复杂性。shell和mfe-basket应用程序有自己单独的webpack.config.js。打开shell或mfe-basket应用程序的projects/shell/webpack.config.js文件,以查看整体结构。这个文件是我们在模块联合插件中添加主机、遥控器、共享代码和共享依赖项的布线的地方。如果您不使用@angular-architects/module-federation依赖项,结构将有所不同,但配置的基本想法保持不变。
让我们来探索一下这个配置文件的部分。
// ...imports here
const sharedMappings = new mf.SharedMappings();
sharedMappings.register(
path.join(__dirname, '../../tsconfig.json'),
[
'@shared'
]);
module.exports = {
// ...other very important config properties
plugins: [
new ModuleFederationPlugin({
library: { type: "module" },
// For remotes (please adjust)
// name: "shell",
// filename: "remoteEntry.js",
// exposes: {
// './Component': './projects/shell/src/app/app.component.ts',
// },
// For hosts (please adjust)
remotes: {
"mfeBasket": "http://localhost:4201/remoteEntry.js",
},
shared: share({
// ...important external libraries to share
...sharedMappings.getDescriptors()
})
}),
sharedMappings.getPlugin()
],
};
在mfe-basket的webpack.config.js中,您将在文件顶部看到@shared的路径和配置,以确定在远程应用程序中要公开的内容。
shell应用程序在端口4200上服务,mfe-basket应用程序在端口4201上服务。我们可以打开两个终端来运行每个应用程序,或者我们可以使用示意图为我们创建的以下npm脚本来add@angular-architects/module-federation:
npm run run:all
当您这样做时,您将看到两个应用程序在浏览器中打开,以及它们如何在端口4200上运行的shell应用程序中组合在一起。单击Baket按钮导航到在mfe-basket应用程序中显示BasketModule的新路由。登录按钮还不能正常工作,但我们接下来会在这里运行。
注意-我本可以用于初学者的另一个选项是Nx工作区。Nx拥有强大的工具和内置支持,用于使用Webpack和模块联合构建微型前端。但我想在项目工具上变得简约,这样你就有机会沉浸在一些配置要求中。
@shared语法对你来说可能看起来有点不寻常。您可能期望看到通往图书馆的相对路径。@shared语法是库路径的别名,该路径在项目的tsconfig.json文件中定义。你不必这样做。您可以使用相对路径离开库,但添加别名使您的代码看起来更干净,并有助于确保代码架构的最佳实践。
由于除了webpack.config.js主机应用程序不知道远程应用程序,我们通过在decl.d.ts中声明远程应用程序来帮助TypeScript编译器。您可以在此提交中查看为初学者所做的所有配置更改和源代码。
使用OpenID Connect添加身份验证
模块联合最有用的功能之一是管理共享代码和状态。让我们通过向项目添加身份验证来看看这一切是如何运作的。我们将在现有应用程序和新的微前端中使用经过身份验证的状态。
在开始之前,您需要一个免费的Okta开发人员帐户。安装Okta CLI并运行okta register以注册新帐户。如果您已经有一个帐户,请运行okta login。然后,运行okta apps create。选择默认应用程序名称,或根据您认为合适的方式进行更改。选择单页应用程序,然后按Enter键。
使用http://localhost:4200/login/callback进行重定向URI,并将注销重定向URI设置为http://localhost:4200。
Okta CLI是做什么的?
The Okta CLI will create an OIDC Single-Page App in your Okta Org. It will add the redirect URIs you specified and grant access to the Everyone group. It will also add a trusted origin for http://localhost:4200. You will see output like the following when it’s finished:
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
NOTE: You can also use the Okta Admin Console to create your app. SeeCreate an Angular Appfor more information.
记下Issuer和Client ID。你很快就会需要这些价值观。
我们将使用Okta Angular和Okta Auth JS库将我们的Angular应用程序与Okta身份验证连接起来。通过运行以下命令将它们添加到您的项目中。
npm install @okta/okta-angular@5.2 @okta/okta-auth-js@6.4
接下来,我们需要将OktaAuthModule导入shell项目的AppModule,并添加Okta配置。将以下代码中的占位符替换为之前的Issuer和Client ID。
import { OKTA_CONFIG, OktaAuthModule } from '@okta/okta-angular';
import { OktaAuth } from '@okta/okta-auth-js';
const oktaAuth = new OktaAuth({
issuer: 'https://{yourOktaDomain}/oauth2/default',
clientId: '{yourClientID}',
redirectUri: window.location.origin + '/login/callback',
scopes: ['openid', 'profile', 'email']
});
@NgModule({
...
imports: [
...,
OktaAuthModule
],
providers: [
{provide: OKTA_CONFIG, useValue: { oktaAuth } }
],
...
})
使用Okta进行身份验证后,我们需要设置登录回调以完成登录过程。在shell项目中打开app-routing.module.ts,并更新路由数组,如下所示。
import { OktaCallbackComponent } from '@okta/okta-angular';
const routes: Routes = [
{path: '', component: ProductsComponent },
{path: 'basket', loadChildren: () => import('mfeBasket/Module').then(m => m.BasketModule) },
{path: 'login/callback', component: OktaCallbackComponent }
];
现在我们已经在应用程序中配置了Okta,我们可以添加代码来登录和注销。在shell项目中打开app.component.ts。我们将添加使用Okta库登录和注销的方法。我们还将更新两个公共变量,以使用实际的身份验证状态。更新您的代码以匹配以下代码。
import { Component, Inject } from '@angular/core';
import { filter, map, Observable, shareReplay } from 'rxjs';
import { OKTA_AUTH, OktaAuthStateService } from '@okta/okta-angular';
import { OktaAuth } from '@okta/okta-auth-js';
@Component({
selector: 'app-root',
templateUrl: './app.component.html',
styles: []
})
export class AppComponent {
public isAuthenticated$: Observable<boolean> = this.oktaStateService.authState$
.pipe(
filter(authState => !!authState),
map(authState => authState.isAuthenticated ?? false),
shareReplay()
);
public name$: Observable<string> = this.oktaStateService.authState$
.pipe(
filter(authState => !!authState && !!authState.isAuthenticated),
map(authState => authState.idToken?.claims.name ?? '')
);
constructor(private oktaStateService: OktaAuthStateService, @Inject(OKTA_AUTH) private oktaAuth: OktaAuth) { }
public async signIn(): Promise<void> {
await this.oktaAuth.signInWithRedirect();
}
public async signOut(): Promise<void> {
await this.oktaAuth.signOut();
}
}
我们需要为登录和注销按钮添加点击处理程序。在shell项目中打开app.component.html。更新登录和注销按钮的代码,如图所示。
<li>
<button *ngIf="(isAuthenticated$ | async) === false; else logout"
class="flex items-center transition ease-in delay-150 duration-300 h-10 px-4 rounded-lg hover:border hover:border-sky-400"
(click)="signIn()"
>
<span class="material-icons-outlined text-gray-500">login</span>
<span> Sign In</span>
</button>
<ng-template #logout>
<button
class="flex items-center transition ease-in delay-150 duration-300 h-10 px-4 rounded-lg hover:border hover:border-sky-400"
(click)="signOut()"
>
<span class="material-icons-outlined text-gray-500">logout</span>
<span> Sign Out</span>
</button>
</ng-template>
</li>
尝试使用npm run run:all运行项目。现在您将能够登录和退出。当您登录时,会显示一个新的个人资料按钮。当您单击它时,什么都不会发生,但我们将创建一个新的遥控器,将其连接到主机,然后在这里共享身份验证状态!
创建一个新的Angular应用程序
现在,您将有机会通过创建显示经过身份验证的用户配置文件信息的微前端应用程序,了解微前端远程如何连接到主机。停止为项目提供服务,并在终端中运行以下命令,以在项目中创建新的Angular应用程序:
ng generate application mfe-profile --routing --style css --inline-style --skip-tests
有了这个Angular CLI命令,你
生成了一个名为mfe-profile的新应用程序,其中包括一个模块和一个组件
在应用程序中添加了一个单独的路由模块
定义了在组件中内联的CSS样式
跳过为初始组件创建相关测试文件
您现在将为默认路由创建一个组件,HomeComponent和一个用于容纳微前端的模块。我们可以连接微型前端,只使用一个组件而不是一个模块。事实上,一个组件将满足我们对配置文件视图的需求,但我们将使用一个模块,以便您可以看到每个微前端如何随着项目的发展而增长。在终端中运行以下两个命令:
ng generate component home --project mfe-profile
ng generate module profile --project mfe-profile --module app --routing --route profile
使用这两个Angular CLI命令,您:
在mfe-profile应用程序中创建了一个新组件HomeComponent
创建了一个新的模块ProfileModule,带有路由和默认组件ProfileComponent。您还使用AppModule的“/profile”路径将ProfileModule添加为懒惰加载路由。
让我们更新代码。首先,我们将添加默认路线。打开projects/mfe-profile/src/app/app-routing.module.ts,并为HomeComponent添加新路由。您的路由数组应与以下代码匹配。
const routes: Routes = [
{path: '', component: HomeComponent },
{path: 'profile', loadChildren: () => import('./profile/profile.module').then(m => m.ProfileModule) }
];
接下来,我们将更新AppComponent和HomeComponent模板。Openprojectsprojects/mfe-profile/src/app/app.component.html并删除其中的所有代码。用以下内容替换它:
<h1>Hey there! You're viewing the Profile MFE project! 🎉</h1>
<router-outlet></router-outlet>
打开projects/mfe-profile/src/app/home/home.component.html,并将文件中的所有代码替换为:
<p>
There's nothing to see here. 👀<br/>
The MFE is this way ➡️<a routerLink="/profile">Profile</a>
</p>
最后,我们可以更新配置文件的代码。幸运的是,Angular CLI为我们处理了很多脚手架。因此,我们只需要更新组件的TypeScript文件和模板。
打开projects/mfe-profile/src/app/profile/profile.component.ts并编辑组件以添加两个公共属性,并在构造函数中包含OktaAuthStateService:
import { Component, OnInit } from '@angular/core';
import { OktaAuthStateService } from '@okta/okta-angular';
import { filter, map } from 'rxjs';
@Component({
selector: 'app-profile',
templateUrl: './profile.component.html',
styles: []
})
export class ProfileComponent {
public profile$ = this.oktaStateService.authState$.pipe(
filter(state => !!state && !!state.isAuthenticated),
map(state => state.idToken?.claims)
);
public date$ = this.oktaStateService.authState$.pipe(
filter(state => !!state && !!state.isAuthenticated),
map(state => (state.idToken?.claims.auth_time as number) * 1000),
map(epochTime => new Date(epochTime)),
);
constructor(private oktaStateService: OktaAuthStateService) { }
}
接下来,打开相应的模板文件,并将现有代码替换为以下内容:
<h3 class="text-xl mb-6">Your Profile</h3>
<div *ngIf="profile$ | async as profile">
<p>Name: <span class="font-semibold">{{profile.name}}</span></p>
<p class="my-3">Email: <span class="font-semibold">{{profile.email}}</span></p>
<p>Last signed in at <span class="font-semibold">{{date$ | async | date:'full'}}</span></p>
</div>
尝试通过在终端中运行ng serve mfe-profile --open来自行运行mfe-profile应用程序。请注意,当我们导航到/profile路由时,我们看到一个控制台错误。我们将Okta添加到shell应用程序中,但现在我们需要将mfe-profile应用程序转换为微前端,并共享身份验证状态。停止提供应用程序,以便我们为下一步做好准备。
适用于您的Angular应用程序的模块联合
我们希望使用@angular-architects/module-federation的原理图将mfe-profile应用程序转换为微型前端,并添加必要的配置。我们将为此应用程序使用端口4202。通过在终端中运行以下命令来添加原理图:
ng add @angular-architects/module-federation --project mfe-profile --port 4202
此示意图如下:
更新项目的angular.json配置文件,为应用程序添加端口,并更新构建器以使用自定义Webpack构建器
创建webpack.config.js文件,并构建模块联合的默认配置
首先,让我们通过更新inprojectsprojects/mfe-profile/webpack.config.js的配置,将新的微前端添加到shell应用程序中。在文件的中间,有一个带有注释代码的plugins属性。我们需要完成配置。由于此应用程序是远程的,我们将在注释下更新代码片段:
// For remotes (please adjust)
默认值大多是正确的,只是我们有一个模块,而不是我们想要公开的组件。如果您想公开组件,您所要做的就是更新要公开的组件。通过匹配以下代码片段,更新配置片段以公开ProfileModule:
// For remotes (please adjust)
name: "mfeProfile",
filename: "remoteEntry.js",
exposes: {
'./Module': './projects/mfe-profile/src/app/profile/profile.module.ts',
},
现在我们可以将微前端整合到shell应用程序中。打开projects/shell/webpack.config.js。在这里,您将添加新的微前端,以便shell应用程序知道如何访问它。在文件中间,在plugins组中,有一个remotes属性。初学者代码mfeBasket中的微前端已添加到remotes对象中。您还将在那里添加mfeProfile的遥控器,遵循相同的模式,但将端口替换为4202。更新您的配置,使其看起来像这样。
// For hosts (please adjust)
remotes: {
"mfeBasket": "http://localhost:4201/remoteEntry.js",
"mfeProfile": "http://localhost:4202/remoteEntry.js"
},
我们可以更新代码以合并配置文件的微前端。打开projects/shell/src/app/app-routing.module.ts。使用路径“配置文件”在路由数组中向配置文件微前端添加路径。您的路由数组应该看起来像这样。
const routes: Routes = [
{path: '', component: ProductsComponent },
{path: 'basket', loadChildren: () => import('mfeBasket/Module').then(m => m.BasketModule) },
{path: 'profile', loadChildren: () => import('mfeProfile/Module').then(m => m.ProfileModule)},
{path: 'login/callback', component: OktaCallbackComponent }
];
这是什么!?IDE将导入路径标记为错误!shell应用程序代码不知道Profile模块,TypeScript需要一点帮助。打开projects/shell/src/decl.d.ts并添加以下行代码。
declare module 'mfeProfile/Module';
IDE现在应该更快乐了。?/p>
接下来,更新shell应用程序中Profile的导航按钮,以路由到正确的路径。Openprojectsprojects/shell/src/app/app.component.html,并找到配置文件按钮的routerLink。它应该大约在第38行。目前,routerLink配置是routerLink="/"但现在应该是
<a routerLink="/profile">
这是我们将微前端远程连接到主机应用程序所需的一切,但我们也希望共享身份验证状态。模块联盟使共享状态成为一块(杯)蛋糕。
微前端状态管理
要共享库,您需要在webpack.config.js中配置库。让我们从shell开始。Openprojectsprojects/shell/src/webpack.config.js。
有两个地方可以添加共享代码。一个地方是项目内的代码实现,一个地方是共享的外部库。在这种情况下,我们可以共享Okta外部库,因为我们没有实现包装Okta授权库的服务,但我将指出这两个地方。
首先,我们将添加Okta库。向下滚动到文件底部的shared属性。您将遵循与列表中已有的@angular库相同的模式,并添加两个Okta库的单例,如此片段所示:
shared: share({
// other Angular libraries remain in the config. This is just a snippet
"@angular/router": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@okta/okta-angular": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
"@okta/okta-auth-js": { singleton: true, strictVersion: true, requiredVersion: 'auto' },
...sharedMappings.getDescriptors()
})
当您在此项目中创建库时,如启动代码中的篮子服务和项目服务,您将库添加到webpack.config.js文件顶部的sharedMappings数组中。如果您创建一个新库来包装Okta的库,这就是您将添加它的地方。
现在您已将Okta库添加到微前端主机中,您还需要将它们添加到消耗依赖项的遥控器中。在我们的案例中,只有mfe-profile应用程序使用Okta认证的状态信息。Openprojectsprojects/mfe-profile/webpack.config.js。像对shell应用程序一样,将两个Okta库添加到shared属性中。
现在,您应该能够使用npm run run:all运行该项目,纸杯蛋糕店面应该允许您登录,查看您的个人资料,退出,并将物品添加到您的纸杯蛋糕篮中!