对前端网络应用的要求继续增长。 作为消费者,我们期望我们的Web应用程序功能丰富,性能高。作为开发者,我们担心如何提供高质量的功能和性能,同时牢记良好的开发实践和架构。
进入微前端架构。微前端的模型与微服务的概念相同,是一种分解单体前端的方式。你可以将微型前端结合起来,形成一个功能齐全的网络应用。由于每个微型前端可以独立开发和部署,你有一个强大的方式来扩展前端应用程序。
那么,微型前端的架构是什么样子的呢?比方说,你有一个电子商务网站,看起来像这个网站一样令人惊叹。

你可能有一个购物车,注册用户的账户信息,过去的订单,支付选项,等等。你也许可以将这些功能进一步分类为域,每一个域都可以是一个独立的微型前端,也被称为remote 。微型前端的远程集合被安置在另一个网站内,即网络应用的host 。
因此,你的电子商务网站使用微前端来分解不同的功能,可能看起来像这样的图,其中购物车和账户功能是在你的单页应用程序(SPA)中的独立路线。

你可能会说,"微前端听起来很酷,但管理不同的前端和协调跨微前端的状态也听起来很复杂。"你是对的。微前端的概念已经存在了几年,推出你自己的微前端实现、共享状态和支持它的工具是一项相当大的事业。然而,微前端现在得到了Webpack 5和Module Federation的良好支持。并非所有的Web应用都需要微前端架构,但对于那些大型的、功能丰富的、已经开始变得笨重的Web应用来说,我们的Web工具中对微前端的一流支持绝对是一个加分项。
这篇文章是一个系列的第一部分,我们将使用Angular和微前端建立一个电子商务网站。我们将使用Webpack 5和模块联盟来支持微前端的连接。然后,我们将演示在不同前端之间共享认证状态,并将其部署到一个免费的云主机提供商。
在这第一篇文章中,我们将探讨一个启动项目,了解不同的应用程序如何连接,使用Okta添加认证,并添加共享认证状态的线路。最后,你会有一个看起来像这样的应用。

先决条件
- Node这个项目是用Node v16.14和npm v8.5开发的。
- Angular CLI
- Okta CLI
目录
- 使用Webpack 5和模块联盟的微前端启动器
- 使用OpenID Connect添加认证
- 创建一个新的Angular应用程序
- 为你的Angular应用程序提供模块联盟
- 微观前端的状态管理
- 接下来的步骤
- 了解Angular、OpenID Connect、微前端等信息
使用Webpack 5和模块联盟的微前端启动器
这个网络应用有很多东西!我们将使用一个启动代码,以确保我们专注于微型前端特有的代码。如果你对使用启动器而不是从头开始感到沮丧,不要担心。我会在版本库的README.md中提供Angular CLI命令来重新创建这个启动程序的结构,所以你有所有的说明。
按照下面的步骤克隆Angular Micro Frontend ExampleGitHub repo,并在你喜欢的IDE中打开该repo。
git clone https://github.com/oktadev/okta-angular-microfrontend-example.git
cd okta-angular-microfrontend-example
npm ci
让我们深入学习代码吧🎉
我们有一个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脚本来添加@angular-architects/module-federation 。
npm run run:all
当你这样做时,你会看到这两个应用程序在你的浏览器中打开,以及它们如何在运行于4200端口的shell 应用程序中配合。点击篮子按钮,导航到一个新的路线,在mfe-basket 应用程序中显示BasketModule 。签到按钮还不能完全工作,但我们接下来会在这里把它弄好。
注意--我可以为启动器使用的另一个选项是Nx工作区。Nx有很好的工具和内置支持,可以用Webpack和Module Federation构建微前端。但我想在项目工具上做得简约一些,这样你就有机会接触到一些配置要求。
@shared 语法对你来说可能看起来有点不寻常。你可能以为会看到一个相对路径的库。@shared 语法是库的路径的别名,它是在项目的tsconfig.json 文件中定义的。你不需要这样做。你可以让库使用相对路径,但添加别名可以使你的代码看起来更干净,并有助于确保代码架构的最佳实践。
因为除了在webpack.config.js ,主机应用程序不知道远程应用程序,我们通过在decl.d.ts 中声明远程应用程序来帮助TypeScript编译器。你可以在这个提交中看到为启动器所做的所有配置修改和源代码。
使用OpenID Connect添加认证
模块联盟最有用的功能之一是管理共享代码和状态。让我们通过向项目添加认证来看看这一切是如何进行的。我们将在现有的应用程序中使用认证状态,并使用一个新的微型前端。
在你开始之前,你需要一个免费的Okta开发者账户。安装Okta CLI并运行okta register ,以注册一个新账户。如果你已经有一个账户,运行okta login 。然后,运行okta apps create 。选择默认的应用程序名称,或根据你的需要进行更改。 选择单页应用程序,然后按回车键。
在重定向URI中使用http://localhost:4200/login/callback ,并将注销重定向URI设置为http://localhost:4200 。
Okta CLI是做什么的?
Okta CLI将在您的Okta机构中创建一个OIDC单页应用。它将添加您指定的重定向URI,并授予Everyone组的访问权。它还会为http://localhost:4200 添加一个受信任的来源。当它完成时,你会看到如下的输出。
Okta application configuration:
Issuer: https://dev-133337.okta.com/oauth2/default
Client ID: 0oab8eb55Kb9jdMIr5d6
注意:你也可以使用Okta管理控制台来创建你的应用程序。更多信息请参见创建一个Angular应用程序。
记下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: '/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。你还使用'/profile'路径添加了ProfileModule,作为懒惰加载的路由,到AppModule。
让我们更新一下代码。首先,我们将添加默认路由。打开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 的模板。打开projects/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 路线时,我们看到一个控制台错误。我们在shell 应用程序中加入了Okta,但现在我们需要把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文件,并为模块联盟搭建出默认配置的脚手架
首先,让我们通过更新projects/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 。在路由数组中使用路径'profile'添加到profile的微前端的路径。你的路由数组应该看起来像这样:
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现在应该更高兴了。 😀
接下来,更新shell 应用程序中Profile的导航按钮,使其指向正确的路径。打开projects/shell/src/app/app.component.html ,找到routerLink 的 "简介"按钮。它应该大约在第38行。目前,routerLink 的配置是routerLink="/" ,但现在应该是
<a routerLink="/profile">
这就是我们需要做的将微前端远程连接到主机应用程序的一切,但我们也想分享认证状态。模块联盟使共享状态成为一块(杯)蛋糕。
微观前台的状态管理
要共享一个库,你需要在webpack.config.js 中配置这个库。让我们从shell 。打开projects/shell/src/webpack.config.js 。
有两个地方可以添加共享代码。一个地方是项目内的代码实现,一个地方是共享外部库。在这种情况下,我们可以共享Okta的外部库,因为我们没有实现包裹Okta auth库的服务,但我将指出这两个地方。
首先,我们要添加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认证的状态信息。打开projects/mfe-profile/webpack.config.js 。像对shell 应用程序那样,把两个Okta库添加到shared 属性中。
现在,你应该能够使用npm run run:all 来运行这个项目,而且cupcake店面应该允许你登录,查看你的资料,注销,并将物品添加到你的cupcake篮子里!
接下来的步骤
我希望你喜欢这第一篇关于创建Angular微前端网站的文章。我们探索了微前端的功能,以及在Angular中使用Webpack的模块联盟在微前端之间共享状态。 你可以在@oktadev/okta-angular-microfrontend-example GitHub repo中的local 分支中,通过以下命令查看本帖的完成代码。
git clone --branch local https://github.com/oktadev/okta-angular-microfrontend-example.git
在下一篇文章中,我将展示如何通过过渡到动态模块加载和将网站部署到免费的云服务提供商来为部署做准备。让我们继续通过阅读 "用Angular安全和部署微前端"来构建网站吧!