首发于M物语
从微前端到微后端,不可思议的前端架构思考

从微前端到微后端,不可思议的前端架构思考

背景

微服务的概念已经过去了好久,微前端也已经实践了一段时间,在去年不同的分享会上都有听到各家公司关于微前端的实践,总体来说,微前端是因为前端架构的不断演进,结合后端微服务的理念而创造出来的,用于解决不同前端框架,甚至相同框架的不同版本,如何结合的问题。

微前端是一种类似于微服务的架构,它将微服务的理念应用于浏览器端,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。

之前和前端框架架构的同学也聊了一下,一直思考的是,微前端目前如何落地,能解决什么问题,似乎目前这个场景更多的是解决历史系统的迁移问题,在新系统上,前端,特别是在一个统一的体系中,很少会出现跨多语言,多架构的场景。更多的是直接重构掉,做一个新的。

既然前端能微前端,后端能不能微呢?微服务在某些场景下,解决了不同语言的互调问题,相对带来的,其实还有配套的管理和调度系统,而前面说到,在单一语言领域内,用户可能更熟悉开发一个大系统,这就造成了系统越来越大,越来越复杂的情况。

在类似阿里的体系中,前端写的 Node.js 系统非常多,特别是中后台系统,搭配管理端和前台,业务可以很复杂,前端可以拆分,而后端基本上都是一个,要微服务化,或者 FaaS 化,总有道不明的风险,这里的风险更多的是人员成本,人们总是说“明明跑的好好的,为什么要升级”。

常见的中后台应用逻辑

这么说我们并不是想开历史的倒车,让熟悉传统开发的用户一步到服务化,甚至 Serverless 还是有一些困难的,特别是面对 http 的传统应用,路由的思维以及方便的开发环境已经非常成熟,不管是开发体验或者底层概念上都很难转变。

我们设想过很多种方法让应用变轻,变薄,变得可维护,可扩展。我们在 Midway 中尝试使用了IoC 的方式进行解耦,使用装饰器和注入替换传统的实例化和调用。

使用了 IoC 之后,代码不依赖于框架,只依赖于装饰器。

但是这样并没有把应用拆小,应用还是原来的应用,逻辑还在。

传统的分布式调用是把系统拆成很多服务,但是这样做在没有成熟的配套前会牺牲可维护性,排查问题也会变得困难,同时资源的开销也会难以评估。

那么,是否有办法既方便的拆分应用,又不影响原本的开发调试,资源评估,甚至是可维护性呢?

答案可能是有的。

我们一向擅长用简单的方式解决复杂的问题,但这次有几个问题。

  1. 本地开发的问题,不能影响开发
  2. 部署的问题,尽量减少成本消耗
  3. 我要拆得简单,容易理解和维护
  4. 面向未来扩展,可迁移到微服务乃至 FaaS 场景

面对这么多问题,我们只能一一来思考和解决。

如何拆分

恰好在去年的 jsconf 2019 上,我们提出过一种把应用变为函数的拆分方法,把路由层(Router,Controller)单单独拆分,同时将,业务逻辑(Service)垂直拆分的设想。

有谁还记得去年的 jsconf 2019 的拆分模型呢。

这种拆分方式将应用分为两类(Controller + Service),同时又将业务本身按照领域进行了划分。假如在这个基础上,我们把路由也进行领域(垂直划分),是非常自然的,而原始目录结构中的 controller 目录和 service 目录也很好的印证了这个想法。

第二个问题是,拆分了之后,代码的组织方式,开发模式有什么变化呢?

开发方式的变化

用户是非常懒和挑剔的,任何增加成本减少收益的事情都是不会干的。如果调整了代码组织方式,并且还要修改代码,甚至调整原来习惯的开发方式的话,都会骂娘的。

代码分离之后,管理方式还好说,现在可以用 git 分仓库管理或者用类似 lerna 的工具来同仓库管理。但是开发方式是实打实的体验,如何在尽可能不修改的情况下来拆分逻辑,增加扩展性呢?

这就需要框架加载方式的支持了。

我们以一个 midway 项目举例,使用了 lerna 管理 monorepo,模拟拆分的情况。

我们简化一下示例目录结构,大致如下。


main 为主 app,而 apibookvideo 都是对应的领域模块(子应用),包含对应领域的能力,比如提供 API,图书服务以及影视服务。

可以看到,我们的子包的目录结构和原本的大应用是相同的,而文件也就是原来领域抽象的文件(原来的代码写的好的话)。

当然,这样拆了之后,肯定不能运行。启动就会报错,找不到子包的文件,我们还需要对框架做一点点调整。

熟悉 Midway 框架的同学可能知道,IoC 是通过扫描目录文件,预先加载到内存中即可拿到实例,不过这个目录结构下,也是无法扫描到的。幸好我们的 IoC 容器支持传入自定义扫描路径,那么只需要做一点点小小的修改,让 IoC 容器能拿到子包的路径就行。

在 monorepo 下,子包也是一般的 npm 包,我们只要让用户定义就行了,比如有一个名叫 configuration.ts 的文件,内容大概是这样。

哇,这下我们只需要在框架里找到子包(lerna 下 resolve 能找到)的地址,加到扫描路径里就行了。按照原来的依赖,我们对相应子包(api)也增加依赖,用于提供 Restful API 服务。


这下,我们的 main 以及 api 包都能独立开发,独立工作了,而且和原来的开发模式完全保持一致。

使用的时候,是通过导入 npm 包的形式来进行协作使用的也是标准的 import 语法,以及 class 形式,没有增加特别的语义,无需去额外学习。

这就是梦想中的模式,我们似乎可以叫他“微后端”。


部署模型

开发模型上,我们把代码变成了子模块,通过 IoC 扫描的方式让代码的开发模式和以前完全一致,那部署呢?

由于主包没有什么太大的变化,发布构建的时候只要单独发布 main 包即可,这种方式特别适合 CRM 后台系统,多路由组合的情况。

最后

得益于 IoC 体系,我们能向其他场景去尝试输出这些能力。在 2019 年底我们开放了 midway-faas 体系的代码,这就出现了一些从 midway 代码转变为 midway-faas 或者复用的需求,那么,我们是不是能够将一部分 midway(service)代码,直接拷贝成一个新的模块(子包)的形式,让 midway-faas 的能够运行这些代码呢?

这其实是个 feature 预告,下一版本 midway,以及 midway-faas 已经支持了这种扩展模型,你可以随时的去扩展自己的可复用能力,也可以拆分自己的应用,构建垂直化领域模型代码,甚至是从 midway 模块迁移到 midway-faas 模块。

技术永无止境,一切皆有可能,尽在 midway。

发布于 2020-03-04

文章被以下专栏收录

    MidwayJS 是淘宝 FED Nodejs 方向的研究小组,除了在本身的 Midway 品牌本身的产出之外,也会分享在日常开发,研究中的一些心得,欢迎投稿。