angular js 使用pdf.js_企业级 Angular — 基于Angular的微前端shell

基于Angular的微前端shell

如上一章所述,有几种方法可以实现基于 SPA 的微前端。本章介绍如何通过七个步骤实现一个按需加载微前端的 shell。除了在有关微前端和 Web 组件的章节中,我们没有将 Web component 用于宏观架构,只使用了按需加载和按需引导的普通 SPA,实际上我们决定将它应用于微观架构。

58de20ff22e04ffabd3ccd849ac81cae.png

我们可以使用 Shadow DOM 隔离不同的应用程序以简化实现。从诞生之日起,Angular 就设计其组件支持 Web Component。

在案例中我们的目标是将两简单的应用 client-a 和 client-b 加载到 shell 中。它们会共享一个 Widget:

f826a1e649025ed22747a2146766ce91.png

你可以在作者的 Github 仓库中找到此源码

第一步:确定你需要这么做

确保此方法符合你的体系架构目标。如上一章所述,微前端也有缺点,你需要对它们有足够的了解。

第二步:实现单页应用

首先将你的子应该实现为一个普通的 angular 应用。

在微服务架构中,每个部分都有一个单独的代码库以尽可能地使它们解耦。在实际中会看到很多基于 monorepos 的微前端。当然,何时应用微前端是一个值得讨论的话题,但与之相比更重要的是找到适合目标系统的架构,并意识到这种架构可能带来的后果。

如果我们使用 monorepo,则必须确保 lint 规则发挥其作用,而不是将微前端彼此耦合。前面已经提到过,Nrwl's Nx为此提供了一个出色的解决方案:它可以限制哪些库可以相互访问。Nx 可以检测到 monorepo 的哪些部分受到更改的影响,并且仅重新编译、测试这些受变化影响的部分。

为了简化微前端的路由,建议将微应用名称作为路由前缀。下面是 client-a 应用的路由代码:

@NgModule({

imports: [

ReactiveFormsModule,

BrowserModule,

RouterModule.forRoot([

{ path: 'client-a/page1', component: Page1Component },

{ path: 'client-a/page2', component: Page2Component },

{ path: '**', component: EmptyComponent}

], { useHash: true })

],

[...]

})

export class AppModule {

[...]

}

第三步:暴露共享的 Widget

暴露那些你需要作为 Web Component 或 Custom Element 使用的 Widgets。注意,从微服务的角度来看,你应该尽量避免微前端之间的代码共享,因为这会导致体系结构之间的耦合。

可以通过 Angular Element 把 Angular 组件作为自定义元素暴露给外部使用。也可以到作者的 Angular Element 和 lazy and external Angular Elements学习相关内容。

第四步:编译单页应用

Webpack 和 Angular CLI 都使用全局数组来注册包,这使得应用程序不同 chunks(lazy chunks)之间能够相互查找。但是,如果我们将多个 SPA 加载到一个页面中,整个过程就会乱掉,无法正常运行。

我们有两种解决方案:

  1. 将所有的内容都放到一个 bundle 中,这样那个全局数组就显得多余。

  2. 重全名那个全局数组。

我们使用了第一种方案,顾名思义,微前端体积应该很小。打包成一个 bundle 可使按需加载变得更加容易。稍后我们会介绍在微前端应用间共享 RxJS 或 Angular 之类的库。

为了调整 CLI 以产生一个捆绑包,这里使用一个工具ngx-build-plus,该工具提供了 --single-bundle 选项:

ng add ngx-build-plus

ng build --prod--single-bundle

在 monorepo 中,还必须加入 project 参数:

ng add ngx-build-plus --project myProject

ng build --prod --single-bundle --project myProject

第五步:创建可按需加载 bundle 的 shell 应用

按需加载 bundle 非常简单。只需要一些原始的 JavaScript 代码即可,主要就是动态创建 script 标签和作为子应用程序的根元素的标签:

//add script tag

const script = document.createElement("script");

script.src = "[...]/client-a/main.js";

document.body.appendChild(script);

//add app

const frontend = document.createElement("client-a");

const content = document.getElementById("content");

content.appendChild(frontend);

当然,你可以把这些都放到一个指令中,另外需要一点代码来控制相应的微应用是否显示:

frontend["visible"] = false;

第六步:微前端之间的通信

通常情况下,我们应该减少微应用之间的通信,这样有利用降低耦合。

实现方式有很多,在这里使用了比较简单的一种方式:参数查询。这种方法有几个优点:

  1. 微前端的加载顺序无关紧要。只要加载后就可以从 URL 中获取当前参数。

  2. 可以使用 deep link。

  3. 和 web 的运作方式一致

  4. 实现起来很简单

在 Angular 中设置 URL 参数是很简单的,如下所示:

this.router.navigate([""], {

queryParamsHandling: "merge",

queryParams: { id: 17 }

});

merge 选项保存现有的 URL 参数。如果已经有一个 id 参数,路由会将其覆盖。

Angular 也提供了监听路由参数变化的方法:

route.queryParams.subscribe(params => {

console.debug("params", params);

});

其它方法:

  1. 如果将微前端包装到 Web 组件中,可以使用属性和事件与 shell 应用进行通信。

  2. shell 应用可以设置一个全局消息队列, typescript(windowasany).messageBus=newBehaviorSubject(null);,shell 和 微应用都可以向这个队列中发布消息,同时也可以监听。

  3. 使用浏览器提供的自定义事件:

// sender

const customer = { id: 17, ... };

window.raiseEvent(new CustomEvent('CustomerSelected', { details: customer }));

// receiver

window.addEventListener('CustomerSelected', (e) => { ... })

第七步:在微前端应用间共享库

由于我们有几个独立的微前端应用,它们每个都有独立的依赖库,例如 Angular 或 RxJS。从微服务的角度来看,这是完美的,因为它允许每个微前端团队选择任何版本的库或框架,他们甚至可以决定是否以及何时更新到较新版本。

但是,从性能和加载时间的角度来看,这种结构是有问题的,因为它导致 bundle 中大量的重复性代码。例如,最终的 bundle 中可能包含好几个版本的 Angular 源码:

035f48ad967988f16b0f3668319c4c66.png

幸运的是,已经有解决方案:Webpack externals

Externals 允许通过将库加载到全局命名空间来共享库。这种方法在 jQuery(提供全局\$对象)时代很流行,有时也适用于简单的 react 和 vue 应用程序。

我们可以对大多数库使用 UMD bundles。只需告诉 webpack 不要将它们与每个微前端打包在一起,而是在全局命名空间中引用它:

87be3c65689b9f7f358c2c014889356a.png

要将 Webpack externals 与 Angular CLI 一起使用,您可以利用ngx-build-plus,它里面已经附带了 schematic 用于帮助完成这种修改。

前面已经介绍过如何安装:

ng add ngx-build-plus

然后调用下面的 schematic

ng g ngx-build-plus:externals

不要忘了,在 monorepo 中需要额外的 project 参数

ng add ngx-build-plus --project myProject

ng g ngx-build-plus:externals --project myProject

这种方法也提供了 npm build::externals 命令 。对于默认项目,也有一个脚本 build:externals。运行此脚本后查看 index.html,会发现直接加载了 Angular 的包:

还可以优化这一过程,将它们放在一个 bundle 中。在生成的 webpack.externals.js 中,可以看到一个包名和全局变量的映射。

const webpack = require("webpack");

module.exports = {

externals: {

rxjs: "rxjs",

"@angular/core": "ng.core",

"@angular/common": "ng.common",

"@angular/common/http": "ng.common.http",

"@angular/platform-browser": "ng.platformBrowser",

"@angular/platform-browser-dynamic": "ng.platformBrowserDynamic",

"@angular/compiler": "ng.compiler",

"@angular/elements": "ng.elements",

"@angular/router": "ng.router",

"@angular/forms": "ng.forms"

}

};

使用此方法使生成的包在需要@angular/core 时引用全局变量 ng.core。因此,@angular/core 不再需要包含在 bundle 中。

请注意,这不是 Angular 的默认模式,可能存在一些风险。

结论

有了正确的思路,为微前端实现 shell 应用并不困难。但是,这只是实现微前端的一种方法,并且有其优点和缺点。在实现之前,请确保它符合你的架构目标。

相关文献

Evans, Domain-Driven Design: Tackling Complexity in the Heart of SoftwareWlaschin, Domain Modeling Made FunctionalGhosh, Functional and Reactive Domain ModelingNrwl, Monorepo-style Angular developmentJackson, Micro FrontendsBurleson, Push-based Architectures using RxJS + FacadesBurleson, NgRx + Facades: Better State ManagementSteyer, Web Components with Angular Elements (article series, 5 parts)

作者

9f5cf4e1ed801b5f4dcb2d07eada2867.png

Manfred Steyer 是一位培训师,顾问和程序设计师,主要关注 Angular。

他在社区的工作得到 Google 认可,授予其成为 Google Developer Expert(GDE)。另外,Manfred 是 Angular 团队中的“受信任的协作者”。他以这个身份为 Angular CLI 实现了差异化加载。

Manfred 曾在 O’Reilly 出版书籍 。也为 German Java Magazine, windows.developer 和 Heise 等杂志撰写文章。

他会定期在有关 Angular 的会议和博客上发表演讲。

在此之前,他负责项目团队主要从事构建 web 业务的相关项目。此外,他在应用科学大学教授了几个有关软件工程的课程。

Manfred 在全职工作同时进行兼职和远程学习,主学位为计算机科学硕士,同时拥有 IT 和 IT Marketing 学位。

您可以在Twitter, Facebook上关注他,并在 http://www.softwarearchitekt.at找到他的博客。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值