在构建企业Angular应用时,我们经常需要开发一些不同的前端来服务不同的用户类型或渠道。虽然它们可能需要不同的UI风格或具有独特的功能,但它们仍然有很多共同的功能。
一种解决方案是为每个前端创建独立的应用程序,作为不同的、独立的项目。也许很明显,这种方法会使代码库膨胀,可能会导致重复,并使你的代码难以维护。
因此,你可能会想,你需要寻找一个单一的Angular项目的解决方案,设法干净地分离不同的UI组件和服务,同时最大化代码共享。这是个相当大的挑战!
但是,就像我们每天做的其他软件设计决定一样,关键是要在这两个问题之间找到一个平衡点:干净分离的代码和可共享的服务和实用功能。
在这篇文章中,我们将探讨如何通过利用懒惰加载功能模块和动态配置来构建一个具有多个前端的Angular应用。
概述
作为一个假想的例子,我们将建立一个有两个前端的电子医疗应用。
- 患者门户:患者可以使用该应用来预约,管理他们的药物,等等。
- 医生门户:医生将能够执行管理任务,管理药物等。
如下图所示,这两个门户暴露了不同的功能,但都建立在相同的框架上,并共享一些共同的功能。
我们想实现的目标是。
- 这两个门户之间有明确的分隔。它们可以有自己的风格或导航,而不影响其他门户
- 能够共享一套共同的代码和功能模块
- 每个前端应用可以独立构建和部署
- 构建的包不会包括没有被使用的模块
设置Angular项目
在我们开始之前,确保你的本地环境安装了以下工具。
- Node.js ≥ v10.x.x
- Angular CLI ≥ v9.x.x
使用下面的Angular CLI命令,我们可以创建一个新的应用骨架。
ng new multipleFront
新创建的应用程序包含一个默认的应用程序模块和一个默认的入口组件。我们可以使用下面的CLI命令添加一个新模块。
ng g module patient --route patient --module app.module
该命令将生成一个病人文件夹,其中包含module,routing, 和component 文件。
要创建一个新的组件,我们可以使用下面的CLI命令。
ng g component home
使用上述CLI命令,我们可以设置我们的项目结构,如下所示。
-- app
-- patient module
|-- [+] home components
|-- patient-routing.module.ts
|-- patient.module.ts
-- doctor module
|-- [+] home components
|-- doctor-routing.module.ts
|-- doctor.module.ts
-- features
|-- [+] admin module
|-- [+] booking module
|-- [+] meds module
- app-routing.module.ts
- app.module.ts
- routes.ts
- app-component.html
在Angular中创建懒惰加载功能模块
在Angular中,所有东西都是以模块的形式组织的。在上一步中,我们生成了默认的app模块,它是app的入口点。我们可以通过引导app模块来启动应用程序。
然后,我们创建了功能模块。它们通常是按领域区域组织的,可以用来把相关的组件、服务和其他功能组合在一起。
一个功能模块有一个根组件,作为该模块的主视图。它承载了模块内所有的子组件*。* 下面的例子显示了管理功能模块和它的根组件:AdminViewComponent 。
-- features
|-- admin module
|-- [+] Admin-View component
|-- admin-routing.module.ts
|-- admin.module.ts
功能模块的一些好处包括能够使用懒惰加载来按需加载它们,并减少主应用程序的捆绑大小。
{
path: 'booking',
data: { title: 'Book appointment' },
loadChildren: () =>
import('../features/booking/booking.module').then(
(m) => m.BookingModule
),
}
在上面的示例路由中,booking 功能模块只在路由被激活时加载。在构建时,为懒惰加载的功能模块创建一个单独的bundle文件,使主bundle文件的大小更小。
通过环境文件定位前端门户
为了针对每个前端门户,我们将为它们创建一个不同的环境文件。这两个环境文件都将位于environment 文件夹下,我们将使用moduleId 来区分病人和医生的门户。
// environment.ts
// default environment targeting the patient portal
export const environment = {
production: false,
moduleId: 'default'
}
// environment.doctor.ts
export const environment = {
production: false,
moduleId: 'doctor'
}
设置前台专用的动态路由
路由是Angular的骨干。不同的前台需要不同的路由。为了只用它所需要的路由来服务每个单独的门户,我们必须在运行时动态地生成路由。
首先,我们在一个文件中定义所有的路由,routes.ts ,以方便维护。
export const routes: Routes = [
{
path: '',
data: { name: 'default', modules: ['all'] },
redirectTo: 'home',
pathMatch: 'full',
},
{
path: 'home',
data: { name: 'home', title: 'Home', modules: ['default'] },
loadChildren: () =>
import('./patient/patient.module').then((m) => m.PatientModule),
},
{
path: 'home',
data: { name: 'home', title: 'home', modules: ['doctor'] },
loadChildren: () =>
import('./doctor/doctor.module').then((m) => m.DoctorModule),
},
];
为什么文件中会有两个Home 路由?这是因为我们有两个门户应用存在于项目中,它们都需要一个Home 路由。
在定义了路由之后,我们设置了APP_INITIALIZER DI令牌,以钩住应用程序的引导过程。
providers: [
{
provide: APP_INITIALIZER,
useFactory: loadRoutes,
deps: [Injector],
multi: true,
},
]
APP_INITIALIZER 令牌代表一个工厂函数loadRoutes 。该函数在应用启动过程中执行。该函数将过滤路由并将路由设置到当前的路由器中,该函数将在应用程序完全启动之前完成。
export function loadRoutes(injector: Injector) {
return () => {
const moduleId = environment.moduleId;
const filteredRoutes = routes.filter((r) => {
return (
r.data?.modules.find((r: string) => r === 'all') ||
r.data?.modules.find((r: string) => r === moduleId)
);
});
const router: Router = injector.get(Router);
router.resetConfig(filteredRoutes);
};
}
在上面的loadRoutes 函数中,路由是由moduleId 配置过滤的。因此,只有相关的路由会被加载到应用程序中。
创建基于路由的动态菜单
作为奖励,我们可以使用动态路由作为数据源来生成菜单。当路由被改变时,菜单将自动更新。
首先,我们创建一个菜单服务。它包含一个menuItems$ 观察变量。
menuItems$: BehaviorSubject<MenuItem[]>;
一旦服务被初始化,我们就填充菜单项。下面代码的要点是。
- 循环浏览路由器配置中的路由
- 对于每个路由,我们调用
configLoader来加载子路由,并将路由数据转换为菜单项 - 将转换后的菜单数据推送到可观察数据流中
this.router.config.forEach((route) => {
const children: any[] = [];
if (route.loadChildren) {
(this.router as any).configLoader.load(this.injector, route).subscribe({
next: (moduleConf: { routes: any[] }) => {
children.push(
...moduleConf.routes.map((childRoute) =>
childRoute.children.map(
(x: { path: string; data: { title: string } }) => ({
path: x.path,
title: x.data?.title,
})
)
)
);
this.menuItems$.next(
this.menuItems$.value.concat
.apply([], [...children])
.filter((x) => x.title)
);
},
});
}
});
菜单服务被注入到menu 组件中。我们可以在两个门户应用中使用相同的menu 组件。同一个菜单组件将动态地加载和过滤菜单项。
为每个UI应用不同的样式
为了给每个应用程序应用不同的样式,我们创建以下scss文件。
styles.scss- 普通风格文件styles-patient.scss- 患者门户的风格文件styles-doctor.scss- 医生门户的样式文件
在Angular.json 文件中,这些样式被映射到不同的构建中。
"doctor": {
"styles": ["src/styles-doctor.scss"],
构建和运行多个前端捆绑包
为了分别构建和运行这两个门户,我们依靠环境配置。
由于应用程序包含两个环境,default 和doctor ,我们需要在Angular.json 文件中添加以下配置。
下面的fileReplacements 设置指定默认的environment.ts 文件在运行时将被environment.doctor.ts 文件替换。
"doctor": {
"fileReplacements": [
{
"replace": "src/environments/environment.ts",
"with": "src/environments/environment.doctor.ts"
}
],
每个门户应用都可以通过以下命令为其环境进行构建。
// package.json
// build patient portal with default configuration
"build": "ng build",
// build doctor portal
"build:doctor": "ng build -c doctor"
要在生产模式下构建应用程序,我们运行以下命令。请注意,doctorproduction 是我们Angular.json 文件中的另一个环境配置。
// package.json
// build the patient portal in production mode
"build:patient:prod": "ng build --prod"
// build the doctor portal in production mode
"build:doctor:prod": "ng build -configuration doctorproduction"
构建输出将被复制到dist 文件夹中,准备发布到webhost。
我们可以使用下面的一个命令来启动该应用程序。
// package.json
// start the app as default patient portal
"start": "ng serve"
// start the app as doctor portal
"start:doctor": "ng serve -c doctor"
现在,我们已经成功地启动了该应用程序!病人和医生门户的主屏幕如下所示。
代码分离和共享
病人门户和医生门户有不同的入口组件,它们作为子组件的容器。每个门户都可以有自己的页眉/页脚组件和独立的CSS样式。当我们改变一个应用程序时,另一个不会受到影响。
如上面的例子应用所示,每个门户都挑选自己的功能模块并懒惰地加载它们。懒惰加载的功能模块被构建在小的捆绑文件中,只有当路由器被导航到时才会被下载到客户端浏览器。例如,当医生门户被部署和运行时,只有医生模块会被加载。病人门户的模块不会被加载,因为它们不在路由中。
这种设计带来了更好的性能,因为每个应用程序不会加载不需要的模块。这对安全性也很有好处--因为病人门户的部署包只建立了相关的模块,所以不可能从病人门户访问只有医生门户的模块!
虽然这两个门户作为不同的应用程序工作,但它们实际上留在同一个项目中。这使得代码共享很容易,并允许将通用框架提取到共享模块中。
总结
在这篇文章中,我们介绍了一个有两个前端的Angular应用程序。它们是用Angular自定义环境配置、动态路由和懒惰加载功能模块构建的。我们实现了清晰分离、轻松共享代码和独立构建输出的目标。
我希望你觉得这篇文章有用。示例项目的源代码,包括我们之前提到的CSS样式文件,都可以在我的GitHub上找到。
The postCustomize Angular lazy loading modules for multiple frontendsappeared first onLogRocket Blog.