如何将 Angular 应用拆分为微前端应用
嗨,好久不见了。希望你最近一切都好。今天,我想给你展示一下我最近在玩的东西。
在这个现代世界中,应用程序似乎在各个方向发展,然后你需要一个更大的团队来处理你的大型应用程序,最终,你发现你有几个较小的团队,每个团队只负责应用程序的一部分,但他们都需要下载、运行、构建和测试整个应用程序。如果你处于这种情况,微前端方法可能更适合你的扩展需求。
什么是微前端?
我不想在这篇文章中重点讨论什么是微前端,但我会尝试简要介绍一下这个概念。现在我们都了解了微服务,它不是用一个庞大的单体应用来处理所有请求,而是将其拆分成多个服务,允许每个服务单独开发、测试、构建和交付。微前端是同样的概念,但应用于前端应用。这种方法允许我们创建较小的应用程序,这些应用程序可以单独开发、测试和构建,并作为大型应用程序的一部分集成,可能使用路由来为每个应用程序提供服务。
从用户的角度来看,他们将使用一个应用程序。从开发者的角度来看,这取决于您希望如何为这些应用程序提供服务。您可以单独为它们提供服务,也可以使用模块联合等高级技术将它们打包成一个应用程序,或者使用其他技术,例如我将要介绍的技术。
对于这个例子,我们将最终构建一个应用程序,它将导入所有其他应用程序,但每个应用程序都可以单独在本地提供服务、测试和部署。
当前微前端的现状
由于微前端允许我们独立开发应用程序,这也意味着你可以使用不同的框架和库来开发应用程序的各个部分。当然,这有其优缺点。如果你使用不同的技术来服务应用程序的不同部分,那么在团队内部调动开发人员就不那么容易了。然而,机会是敞开的,如果你想这样做,你可以使用像Single SPA或其他类似的工具来帮助你将不同的技术组织和连接到一个更大的应用程序中。
你也可以只使用一个前端工具,比如 React、Vue,或者在本例中是 Angular。它们各自都有一些方法来实现我们的需求。如果你更喜欢这种方式,可以使用Nx工具。它有一些非常酷的功能,比如依赖图、链接包等等。它还对模块联合和微前端开发提供了一流的支持。
最后,我们希望能够独立构建、测试和部署每个应用程序,并像使用一个应用程序一样为用户提供所有应用程序。
怎么分割九头蛇?
你可能有一个大型应用程序,并且注意到测试需要很长时间才能完成,构建时间甚至更长。即使是本地服务,该应用也似乎需要很长时间。你可能读过一些关于微前端和将应用拆分成小应用的内容,这些内容可以帮助你减少所有这些时间。你说得对。
我知道我之前说过,但我还是想再说一遍:实现微前端有很多替代方案。你肯定会找到其他指南介绍其他方法,甚至可能找到一些与我使用的方法相同的指南。
我发现的那些描述了如何从头开始使用微前端方法,当然,如果你正在启动一个项目,并且你知道未来会变得足够大而需要这种架构,也许你可以先检查一下这些,看看这是否更适合你需要的东西。
但这并不是使用微前端从头开始创建应用程序,而是将您可能已经使用 Angular CLI 创建的现有应用程序重构为共享库和单个应用程序,这些共享库和单个应用程序可以无缝集成到同一个更大的应用程序中。
审查基础申请
我们将使用这个应用进行实验。您可能会注意到,它实际上不是一个大型应用,因为它甚至不是一个真正的应用。但您可能还会注意到,该应用使用了一些服务,这将使我们能够了解这些服务如何与各个应用集成。
该应用程序的基本结构与使用 Angular CLI 创建新项目时的结构相同。应用程序中包含一些服务、模型和功能模块。此操作的目标是将服务和模型重构为可共享的库,并将功能模块重构为各自的应用程序。
- src
+ features
- add-user
- dashboard
- login
- user-list
- models
+ shared
- auth
- users
路由当前使用延迟加载,如下面的代码摘录所示:
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
import { isLogged } from './shared/auth/is-logged.guard';
import { isNotLogged } from './shared/auth/is-not-logged.guard';
const routes: Routes = [
{
path: '',
canMatch: [isLogged],
loadChildren: () =>
import('./features/dashboard/dashboard.module').then((m) => m.DashboardModule),
},
{
path: '',
canMatch: [isNotLogged],
loadChildren: () => import('./features/login/login.module').then((m) => m.LoginModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
您可以在此 repo 中找到我们将要使用的源代码
轻松开始
迁移复杂的应用程序并非易事,我将尝试提供一些清晰的路径供您参考。但是,如果您考虑到可能需要额外的工具,尤其是用于更好地识别文件之间的依赖关系的工具,那将会有所帮助。
因为首先我们要识别一些没有依赖关系但依赖它们的文件,所以创建一个新的库并将这些文件移动到该库中。在本例中,我们有一个models
文件夹,其中包含应用程序中唯一使用的模型,该模型不依赖于任何其他文件,但可跨服务和功能模块使用。
现在,为了创建库,我们将使用以下 Angular CLI 命令
ng generate library models
这将创建一个名为 projects 的新文件夹,我们的库将创建在该文件夹中。此外,这将安装ng-packagr
为根依赖项,更新angular.json
文件以包含models
库项目描述,并tsconfig.json
使用指向库构建版本的路径映射来更新文件。默认情况下,路径将是库的名称,但我建议将其更新为不太可能与实际 npm 模块冲突的名称。我@@
在这里使用 为前缀,因为不能使用两个 @ 符号作为 npm 模块名称。
此外,您还可以在库文件中更新名称
package.json
。您可能会收到警告,但由于这不会在寄存器中发布,所以应该没问题。但是,如果您想在寄存器中发布,则应该使用寄存器安全的名称。
创建库后,我们将把models
文件夹内容移到这个新库中。您可能会注意到,该库默认会为您创建一个组件、一个模块和一个服务,我们可以将它们全部删除,然后将用户模型文件放入lib
该库的文件夹中。更新public-api.ts
文件以导出lib
文件夹的公共内容。最终,您应该得到如下内容:
要移动文件,您可以使用 而
git mv
不是操作系统功能,以便 git 更好地跟踪更改。此外,您可以根据需要查看此命令生成的其他文件,但我们稍后再处理它们。
之后,我们将更新导入以使用这个库。因此,不要这样使用:
import { User } from 'src/app/models/user';
我们将更新至:
import { User } from '@@models';
记住使用文件中的路径
tsconfig.json
。
现在你应该会遇到一些错误,因为我们还没有构建库。所以,如果我们运行:
ng build models
Angular CLI 将构建模型库,现在 TS 将正确获取您的@@models
引用。
我们已经成功了一半
嗯,其实不算,但我觉得这有点长了。总之,将所有非依赖代码迁移到库中后,就可以开始识别一些可以隔离到库中的依赖代码了。继续这个例子,我们现在要迁移其中一个服务,特别是 auth 文件夹,其中包括 guards。这和模型迁移的步骤一样,创建库,将文件夹内容移动到该lib
文件夹中,构建并更新引用。
运行 Angular CLI 命令来创建auth
库。
ng generate library auth
删除文件夹内容lib
,并将所有auth
文件夹内容移至该lib
文件夹。最后,更新public-api.ts
文件。您应该看到类似以下内容:
你的public-api.ts
文件应该包含如下内容:
/*
* Public API Surface of auth
*/
export * from './lib/auth.service';
export * from './lib/is-logged.guard';
export * from './lib/is-not-logged.guard';
运行 Angular CLI 命令来构建auth
库。
ng build auth
您现在可以更新身份验证服务和防护的导入以使用该库。
恭喜!
您已将应用程序的某些部分迁移到库中。如果您正确执行了此操作,应用程序仍在运行!甚至可能速度更快,因为 Angular 无需构建整个应用程序即可为其提供服务。现在您拥有多个库(在我们的例子中是两个),并且分别进行测试和构建。
在这个例子中,应用程序只共享一个模型和几个服务,但在更大的应用程序中,您可以共享组件、服务、管道和其他部分。
此外,要为每个要共享的部分创建一个新的库,您可以使用“辅助入口点”功能
ng-packagr
将常用内容(例如组件或服务)分组,然后根据更具体的功能将它们分组。但请注意,这需要对配置进行额外的更新。
迁移第一个应用程序
到目前为止,我们已经了解了如何将代码的简单部分迁移到库中。这将使我们能够在应用程序之间重用它们,但我们也在学习如何打包库。
要迁移第一个应用,我建议至少将所选应用使用的所有共享构件都保存在各自的库中。您可能会注意到我们仍然保留着该users
服务。但由于我们要迁移的模块没有使用此服务,因此我们可以稍后安全地迁移该服务。
如果第一次没有成功,也不用担心。像这样的迁移需要时间。我只是向你展示我是如何做到的,而不是我失败的次数。记住,在这种情况下,Git 是你最好的朋友,根据需要创建尽可能多的分支。
我们要做的第一件事是确定要迁移的功能模块。就本例而言,如果你查看过代码,你可能已经猜到了,我们将迁移的是“登录”功能模块。
我们可以使用 Angular CLI 命令创建应用程序:
ng generate application login --style=scss --routing
是
--style=scss
可选的,您可以使用您想要的样式,但--routing
强烈建议使用,您很快就会明白为什么。
这样,我们将在文件夹login
中创建一个新的应用程序projects
,并angular.json
使用该应用程序进行更新。要尝试最近创建的应用程序,您需要运行 Angular CLI 命令:
ng serve login
如果您正在运行主应用程序,您将看到此消息:
? Port 4200 is already in use. Would you like to use a different port?
您可以选择是,然后使用随机端口,更新
angular.json
文件或使用--port
CLI 中的选项,以改为使用固定端口。
我们将把功能模块移动到应用程序中,但不会像库那样替换现有文件,而是在新应用程序的文件夹feature
中创建一个文件夹,并将该文件夹移动到其中。你应该看到类似这样的内容:src
login
我们将更新login
应用程序以使用根路由为此功能模块提供服务。
// app-routing.module.ts
import { NgModule } from '@angular/core';
import { RouterModule, Routes } from '@angular/router';
const routes: Routes = [
{
path: '',
loadChildren: () => import('./feature/login/login.module').then((m) => m.LoginModule),
},
];
@NgModule({
imports: [RouterModule.forRoot(routes)],
exports: [RouterModule],
})
export class AppRoutingModule {}
<!-- app.component.html -->
<router-outlet></router-outlet>
如果所有操作都正确,现在应该有一个未带样式的应用程序。这是因为原始应用程序使用的是 Material 样式,我们也需要为这个应用程序设置 Material SCSS。目前,我们只需将styles.scss
主应用程序中的文件复制粘贴到这个应用程序中即可。在后续文章中,我们将了解如何在应用程序之间共享样式。
此后,应用程序应该可以运行并且看起来良好。
将登录应用程序集成到主应用程序中
我们已经启动并运行了独立的应用,但是我们仍然需要一些方法将它与主应用集成。为此,我们要做的是构建应用,并使用主模块中已有的延迟加载功能将其导入。不过,我们打算将应用构建为一个库。
为了完成设置,我们需要在angular.json
文件中手动创建项目配置以及一些将要使用的文件ng-packagr
。幸运的是,我们已经有一些库,可以用作需要创建的库的模板。
更新angular.json
找到一个库配置,并将其全部复制粘贴到login
配置下方,以便对它们进行分组。将配置的名称(键)更改为login-lib
。将root
、sourceRoot
和其他引用原始库的文件更新为对登录应用程序的引用。通常,您需要将库的原始文件夹名称重命名为login
,但对于 TypeScript 配置文件,您可以创建它们,或使用您已有的文件(出于某种原因,应用程序的创建文件比库少一个)。
配置看起来应该是这样的:
如您所见,我们没有该ng-package.json
文件,因此让我们创建它。
创建ng-package.json
文件
该ng-package.json
文件描述了如何ng-packagr
打包应用程序。它与常规文件没有任何关系package.json
。
我们将从ng-package.json
现有库之一复制文件并将其粘贴到login
文件夹中,即登录应用程序的根文件夹。打开文件,您会注意到三个键:$schema
、dest
和lib
。第一个键用于帮助某些编辑器验证 JSON 文件的模式,第二个键用于定义库的构建位置,最后一个键是用于配置库的对象,包括entryFile
。我们将把dest
值更新为"dest": "../../dist/login-lib"
。这将告诉我们ng-packagr
将应用程序构建为该文件夹中的库。 的内容ng-package.json
应该是这样的:
再次,您可能会注意到我们没有该public-api.ts
文件。
创建public-api.ts
文件
有了这个,我们不需要复制和粘贴,因为这与图书馆通常使用的不同。
在该文件夹下创建一个文件src
并将其命名为public-api.ts
。打开它并仅导出登录功能模块。
// public-api.ts
export { LoginModule } from './app/feature/login/login.module';
建立图书馆
你可能已经猜到了,但这里的目标是创建一个只导出功能模块的库。为此,在登录应用程序的根文件夹中也ng-packagr
需要一个package.json
。你可以从其他库复制一个,但请务必更新项目名称。
现在运行 build Angular CLI 命令
ng build login-lib
并且您应该有一个新建的库功能模块。
导入库
我们需要创建的所有文件,都是在使用该命令时创建的ng generate library
。此外,它还会将tsconfig.json
文件更新到这些库的构建版本的路径映射中。我们接下来就是这么做的。
打开tsconfig.json
,并在paths
对象中添加另一个键,其名称与库的名称相同,并将路径指向"dist/login-lib"
。您的路径配置应如下所示:
如果您使用其他名称来映射您的路径,没问题,只需确保您正确指向每个库的构建版本。
更新您的之后tsconfig.json
,您唯一需要做的就是更新app-routing.module.ts
。
从此:
loadChildren: () => import('./features/login/login.module').then((m) => m.LoginModule),
我们将更新至:
loadChildren: () => import('@@login').then((m) => m.LoginModule),
此后,您应该能够照常处理该申请。
您的第一个应用程序已迁移!
太棒了!如果你继续操作,现在就可以安全地为应用程序提供服务,它会像以前一样工作。但在内部,你现在已经为应用程序的每个部分分离了项目,你可以在其中运行单独的命令,从而缩小每个命令的作用域,并缩短每个命令的运行时间。
机会
使用当前代码,每次克隆代码或更新库(包括其中一个应用)时,我们都需要手动运行构建命令。但是,由于它们没有打包在一起,我们不需要对它们的依赖项执行此操作,假设我们没有破坏任何功能,所有依赖项都可以正常工作。此外,这也意味着我们没有使用版本来处理每个库,因此引入重大更改可能会变得困难。
我们可以使用类似Lerna 的工具来改进部分功能。只要配置正确,Lerna 就能发挥巨大作用。
就这样吧,伙计们!
一如既往,感谢您的阅读。非常感谢大家抽出时间阅读我的文章。我们现在的成员已经超过 5000 人了,所以非常感谢大家。我真心希望这篇文章对您有所帮助。我认为没有必要再写一篇关于迁移其他功能模块的文章,但我会尝试写一篇关于共享样式的文章,或者一些关于配置辅助端点的文章ng-packagr
。我还可以写一篇关于配置 Lerna 以配合使用并改进依赖关系构建的文章。但我认为如果您在评论区告诉我哪些内容对您来说更有趣,那就更好了。
文章来源:https://dev.to/michaeljota/how-to-split-an-angular-app-into-micro-frontend-apps-1fi9