为什么会有 Engage 这么一个平台?Engage 平台究竟是干什么的?

# 插件机制

# 复杂企业 Web 系统的架构历程

假设我们要做一个复杂的企业 Web 系统,涉及到几个、十几个甚至几十个较为独立的模块,每个模块可能有前端、后端或者两者皆有(大部分情况下是前后端都有)。那我们怎么来构建呢?

一开始我们可以做得很简单,前后端融合在一起,一个大工程(文件夹)搞定。我们可能会封装一些业务无关的、可复用的代码形成代码包(即新的工程文件夹),以便降低大工程的复杂度。我们也可能会在大工程下通过分文件夹来提供工程化能力,降低众多人员协作、同时开发的耦合与冲突。

但是这样的系统随着时间的推移,会变得越来越难以维护。代码稳定性、运行稳定性都会变得越来越脆弱。而当面对以下场景时,问题可能会变得更加艰难:

  1. 模块之间互相引用,如函数调用,甚至页面内控件的引用。
  2. 开发团队分散在多个地域,彼此沟通不是很方便。
  3. 系统在部署到客户本地化时,客户可能不会购买所有的模块。客户未购买的模块我们希望不要部署。
  4. 系统如果支持 SaaS 运营,那同样我们也希望客户能够灵活购买模块。不购买的模块就看不到。
  5. 客户需要做定制化,不仅是后端、数据层面,甚至前端页面也要定制化;
  6. 客户可能要求本地化部署,也可能要求 SaaS + 本地化混合部署。

那怎么解决以上这些问题?

第 1 个问题和第 2 个问题,在这么多年的微服务架构上已经得到较大的解决。把一个系统拆成多个服务,每个服务有独立的工程,独立开发、测试、部署、维护。一个小组负责一个或若干个服务。服务与服务之间通过明确的、稳定的网络接口进行通信。而对于前端,同样的,我们可以用微前端的架构模式去开发。微前端的特性跟微服务基本相似,区别在于所有的微前端最终都运行在一个浏览器进程中,它们之间的通信只能限于 DOM 事件、JS 对象等,无法做到像后端那样更大程度的解耦。

后面几个问题,基本上可以归结为如何解决定制化,或者说如何做系统、模块的功能扩展。同时,我们要把模块之间的依赖进行解耦,使它们在自由组合时不至于产生依赖被破坏的问题。最简单的做法就是复制代码——针对客户的需求改一版。但是这无疑增加了维护难度,很容易导致软件质量下降,维护成本提高。

那有没有更好的做法?我们以两个主要场景来举例说明。

# 前端页面定制化

假设我们现在要构建客户管理系统,系统有客户模块和公众号模块。一个客户可能会关注多个微信公众号,也因此会产生多个微信粉丝。在客户的详情页面下,我们需要展示该客户对应的粉丝列表。最终界面可能如下图所示,红色框内即是该客户的粉丝列表。

那么该如何实现呢?在实现这个功能之前,有几点要求我们需要提前说明:

  1. 当企业没有购买公众号模块的时候,客户详情下不应该展示粉丝列表。
  2. 即便企业购买了公众号模块,如果某个用户没有公众号模块的权限,也不应该对这个用户展示粉丝列表。
  3. 企业如果对客户有自己的关联对象(如关联员工),也可以在客户详情下展示(放到粉丝列表前面或后面都行)。

前两者属于功能的灵活组装,末者就是企业特定的定制化需求了。

那怎么去解决这个问题呢?我们一步一步来看一下。首先,这意味着客户模块依赖了公众号模块。看到依赖我们就应该警觉,依赖导致耦合,耦合导致可扩展性差。在不考虑依赖耦合的情况下,我们有下面几种简单的选择:

  1. 公众号模块给客户模块提供一个接口,客户模块的前端开发并渲染出这个列表并调用该接口填充数据。
  2. 公众号模块给客户模块提供一个组件,客户模块直接引入该组件并在指定位置渲染出来。组件内部自行获取数据并填充(对客户模块来说是一个黑盒)。

但是这两种都不可避免地导致客户模块 依赖公众号模块,因为客户模块需要 ”事先“ 知道公众号模块给他提供的接口或组件地址、使用方式等。但是它却很难事先知道公众号模块是否已部署(存在)——因为在某些环境上可能不部署,或者某些企业(租户)可能不购买,这样就可能会导致报错、用户体验上的不友好等情况。

我们知道,在设计模式中,解决依赖(高耦合)的办法有依赖注入(或者叫控制反转)。利用第三方角色以及配置(而不是编码)来实现解耦。所以在这里,客户模块可以定义一个接口,这个接口有特定的标识、需要的组件地址、组件需要接受客户 Id 传递等规范。公众号模块实现了该接口,并通过平台提供的配置化方式注入到客户模块下。当用户打开客户详情页面时,客户详情页面前端向平台查询该接口的实现列表。在环境部署、租户购买公众号模块的情况下,就可以返回公众号的特定实现。因此就能够渲染出粉丝列表这个界面。同理,企业定制化场景下的其他实现也可以自由注入。

我们把上述机制就称作为插件机制(Addon),客户模块定义的接口我们称之为插槽,公众号模块的实现称之为插件。客户插槽定义了 Id 以及它需要的参数,公众号模块进行注册,并提供参数值。在这里,最主要的一个参数就是组件地址。公众号模块的前端输出(export)一个组件,然后将该组件名称通过配置注册告诉给客户模块的前端,客户模块的前端拿到组件对象后就可以进行渲染了。

# 后端接口定制化

除了前端界面的插件以外,我们还可能会遇到后端的插件。假设客户模块在建立一个新客户时,会给企业的客户部门经理发送系统通知。这是标准版的能力。但是这个能力未必能满足企业的需求。客户经理不可能总是登录着系统,可能无法及时收到系统通知。企业有自己的内部通讯工具,它希望新客户通知可以通过内部通讯工具即时推送。

我们不可能为了企业的需求,就将其特定的代码加入到我们的标准版产品中。那这里怎么办呢?跟前面页面定制化一样,我们同样可以用插件机制来解决这个问题。客户模块只需要定义个叫做“新客户通知”的插槽,其参数为一个 HTTP 接口地址(URL),同时规定会对该接口送入什么内容(主要是客户信息)。企业的定制化应用就可以给客户模块注入一个插件,给一个确定的 API URL。这样就能实现针对此企业(租户)的定制化了。

# 插件机制的一些说明

当然,插件机制不是在任何场景下都是最好的。比如上面的后端接口定制,也许客户模块内部就能开发一个更强大的配置工具来支持租户级别的定制化。但是插件机制,是模块(也就是我们后面要说的应用)之间的互相扩展的最通用、最轻量的方式。它能够实现在自由灵活组装模块的基础上,达到最大化的可扩展性。

# aPaaS 平台

以往我们要开发一个新的系统,往往从 0 开始。我们先调研技术栈,再选型,然后开始开发。我们要关心怎么存储文件、怎么存储数据,还要关心怎么去做会话,怎么解决安全性等问题。我们要关心的非业务性需求与功能太多了,我们难道不会厌烦、厌倦吗?

Engage 的目标,是把这些基础架构层面问题都解决掉。让开发人员更专注于业务实现、更在乎客户成功,而不是技术实现、技术成功。Engage 致力于成为一种应用开发平台,提供应用开发所需要的各种公共能力。包括但不限于:

  1. 多租户:
  2. 权限机制:通过用户-岗位-角色-接口或页面地址这种关系,实现接口级、页面级乃至按钮级的权限控制
  3. 版本(Plan):版本代表一个应用不同层级的能力(如配额)。每个租户可以选择购买应用的某个版本。
  4. 对象存储:提供内置的文件上传、图片压缩等能力,包括前端组件
  5. 短地址:将过长的 URL 转为较短的地址以方便在 C 端分发

在不远的将来,Engage 还会提供可视化前端开发(所见即所得)、配置式后端开发,甚至 CI&CD、中间件部署、一键运维等一系列能力。通过这些能力,让我们的应用开发者从重复性的编码等工作中解脱出来,更快、更好地去实现客户价值!