我们生活在一个微服务的世界里,而且这个世界将继续存在。后端开发者需要深入研究领域驱动设计,编写无状态、有弹性、高可用的服务,通过变更数据捕获保持数据同步,处理网络故障,处理认证、授权、JWT......并公开一个漂亮的超媒体API,以便前端开发者可以为其添加一个伟大的用户界面。
很好!但是前端怎么办?
假设我们有几个微服务,几个团队,几个API,因此也有几个用户界面。你怎样才能创建一个集成的、一致的用户界面,使你的用户实际上看不到有多少微服务就有多少用户界面?在这篇文章中,我将使用Angular库实现客户端UI组合设计模式。
一个微服务架构
假设我们有一个CD BookStore应用程序,允许客户在线购买CD和书籍,也允许管理员检查库存。我们也有一些复杂的ISBN号码生成器需要处理。如果我们将这个应用程序建模为微服务,我们最终可能会将其分割成3个独立的微服务:
- 商店:处理显示CD和书籍的信息,允许用户购买。
- 库存:允许管理员检查仓库中物品的可用性,如果需要,可以购买新的物品。
- 生成器:在每次创建新书时生成ISBN号码。
我们最终有两个角色(用户和管理员)通过一个用户界面与这3个API进行交互:
客户端UI构成
在一个完美的微服务世界里,这3个微服务是独立的,由3个不同的团队开发,因此,有3个不同的用户界面,每个都以自己的速度发布。一方面,我们有3个不同的UI,另一方面,我们希望我们的用户能够浏览到他们所看到的一个单一的、集成的应用程序。有几种方法可以做到这一点,而我在这里描述的一种方法被称为客户端UI组合设计模式。
问题
如何实现一个显示多个服务数据的UI屏幕或页面?解决方案
每个团队开发一个客户端UI组件,比如Angular组件,实现他们服务的页面/屏幕的区域。一个UI团队负责实现页面骨架,通过组合多个特定服务的UI组件来构建页面/屏幕。
我们的想法是,在客户端将我们的3个用户界面聚合成一个单一的界面,并最终得到类似这样的东西。
如果使用兼容的技术,将几个用户界面聚合成一个单一的界面效果会更好。在这个例子中,我使用Angular,而且只使用Angular来创建三个用户界面和页面骨架。当然,Web Components也非常适合这个用例,但这并不是本文的目的。
Angular库
自Angular CLI 6以来,一个新奇的功能是能够轻松地创建库。回到我们的架构,我们可以说页面骨架是Angular应用程序(CDBookStore应用程序),然后,每个微服务的用户界面是一个库(商店、库存、发电机)。
在Angular CLI命令方面,这就是我们的情况:
\[sourcecode language="shell"\]
\# 主应用程序叫CDBookStore
$ ng new cdbookstore -directory cdbookstore -routing true
\# 我们的3个微服务UI的3个库
$ ng generate library store -prefix str
$ ng generate library inventory -prefix inv
$ ng generate library generator -prefix gen
\[/sourcecode\]
一旦你执行了这些命令,你将得到以下目录结构:
-
- src/app/是良好的老式Angular结构。在这个目录中,我们将有CDBookStore应用程序的页面骨架。正如你将看到的,这个骨架页面只是一个侧边栏,用来从一个微服务用户界面导航到另一个:
- projects/是所有库的目录
- projects/generator: Generator微服务的用户界面
- projects/inventory:Inventory微服务的用户界面
- projects/store: Store微服务的用户界面
骨架页面
骨架页面是为了聚合所有其他的组件。基本上,它只有一个侧边栏菜单(允许我们从一个微服务UI导航到另一个)和一个。它是你可以拥有登录/注销和其他用户偏好的地方。它看起来像这样:
在代码方面,尽管使用了Bootstrap,但没有什么特别之处。有趣的是定义路由的方式。AppRoutingModule只定义了主程序的路由(这里是主页和关于菜单)。
\[sourcecode language="javascript" title="app-routing.module.ts"\]
const routes:Routes = \[
{path:", component:HomeComponent},
{path: 'home', component:HomeComponent},
{path: 'about', component:AboutComponent},
\];
@NgModule({
imports:\[RouterModule.forRoot(routes)\],
exports:\[RouterModule\]
})
export class AppRoutingModule { }
\[/sourcecode\]
在侧边菜单栏的HTML一侧,我们可以找到导航指令和路由器:
\[sourcecode language="html" title="app.component.html"\]
<ul class="list-unstyled components">
<li>
<a \[routerLink\]="\['/home'\]">
<i class="fas fa-home"></i>
首页
</a>
<a \[routerLink\]="\['/str'\]">
<i class="fas fa-store"></i>
商店
</a>
<a \[routerLink\]="\['/inv'\]">
<i class="fas fa-chart-bar"></i>
存货
</a>
<a \[routerLink\]="\['/gen'\]">
<i class="fas fa-cogs"></i>
Generator
</a>
/li>
</ul>
<!- 页面内容 ->
<div id="content">
<router-outlet></router-outlet>
</div>
\[/sourcecode\]
正如你在上面的代码中看到的,routerLink指令被用来导航到主要的应用程序组件以及库组件。这里的诀窍是,/home被定义在路由模块中,但不是/str、/inv或/gen。这些路由是在库本身中定义的。
免责声明:我是一个糟糕的网页设计师/开发员。如果有人看到这篇文章,知道JQuery和Angular,并想帮我一把,我很想让侧边栏可以折叠起来。我甚至为你创建了一个问题;o)
库
现在,当我们点击商店、库存或发电机时,我们希望路由器能够导航到库的组件:
注意路由器之间的父/子关系。在上面的图片中,红色的路由器出口是父级,属于主应用程序。绿色的router-outlet是子节点,属于库。注意,在store-routing.module.ts中,我们使用str有主路径,所有其他的路径都是childs(使用children关键字)。
\[sourcecode language="javascript" title="store-routing.module.ts" highlight="3″\]
const routes:Routes = \[
{
path: 'str', component:StoreComponent, children:\[
{path:", component:HomeComponent},
{path: 'home', component:HomeComponent},
{path: 'book-list', component:BookListComponent},
{path: 'book-detail', component: BookDetailComponent}:BookDetailComponent},
\]
},
\] 。
@NgModule({
imports:\[RouterModule.forRoot(routes)\],
exports:\[RouterModule\]
})
export class StoreRoutingModule {
}
\[/sourcecode\]
有一个父/子路由器关系的好处是,你可以从 "功能 "导航中获益。这意味着,如果你导航到/home或/about,你会留在主程序中,但如果你导航到/str、/str/home、/str/book-list或/str/book-detail,你会导航到Store库。你甚至可以在每个功能的基础上进行懒惰加载(只在需要时加载商店库)。
Store库在顶部有一个导航条,因此它需要routerLink指令。注意,我们只需要使用子路径[routerLink]="['book-list']"而不需要指定/str(不需要[routerLink]="['/str/book-list']")。
\[sourcecode language="html" title="商店。html"\]
<nav>
<div class="collapse navbar-collapse">
<ul class="navbar-nav mr-auto">
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle">Items</a>
<div class="dropdown-menu" aria-labelledby="navbarDropdownMenuLink">
<a class="dropdown-item" \[routerLink\]="\['book-list'\]">书籍</a>
<a class="dropdown-item" \[routerLink\]="\['cd-list'\]">CD</a>
<a class="dropdown-item" \[routerLink\]="\['dvd-list'\]">DVDs</a>
</div>
</li>
</ul>
</div>
</nav>
<!- 页面内容 ->
<div id="content">
<router-outlet></router-outlet>
</div>
\[/sourcecode\]
优点和缺点
客户端UI组合设计模式有一些优点和缺点。如果你已经有了每个微服务的用户界面,而且每个微服务都使用了完全不同的技术(Angular vs React vs Vue.JS vs ...)或不同的框架版本(Bootstrap 2 vs Bootstrap 4),那么聚合它们可能会很麻烦,你可能不得不重写一些片段以使它们兼容。
但这实际上是有好处的。如果你没有成百上千的开发团队,你可能最终会成为从一个团队转移到另一个团队的人,并且会很高兴发现(或多或少)相同的技术。这在底盘模式中被定义。对于你的最终用户来说,应用程序将具有相同的外观和感觉(在手机、笔记本电脑、桌面、平板电脑上),这给人一种整合的感觉。
以Angular库的结构方式,拥有一个单一的repo要容易得多。当然,每个库都可以有自己的生命周期,并且是独立的,但在一天结束时,把它们放在同一个 repo 上会更容易。我不知道你是否喜欢MonoRepos,但有些人喜欢;o)
结语
我们架构和开发微服务已经有几十年了(是的,它曾经被称为分布式系统,或分布式服务),所以我们大致知道如何让它们在后端工作。现在,我们面临的挑战是,如何让一个用户界面与不同团队开发的多个微服务进行沟通,同时让终端用户感到一致和集成。你可能并不总是有这种需要(看看亚马逊是如何为其服务提供完全不同的用户界面的),但如果你有这种需要,那么客户端UI组合设计模式就是一个很好的选择。
如果你想尝试一下,请下载代码,安装并运行它(只有前端,没有后端API)。不要忘记给我一些反馈。我很想知道你是如何将几个用户界面聚合成 "一个 "的。