为什么会有 Engage 这么一个平台?为了让大家更好地理解 Engage 平台,形成一定的共识,这里我们先讲一下背景。

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

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

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

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

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

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

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

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

# 前端页面定制化

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

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

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

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

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

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

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

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

我们把上述机制就称作为插件机制(Addon),客户模块定义的接口我们称之为插槽,公众号模块的实现称之为插件。

# 后端接口定制化

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

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

# 插件机制的一些说明

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

# 其他公共能力

除了插件机制,Engage 平台会聚焦于提供更多的公共能力,包括但不限于:

  1. 多租户
  2. 权限机制:通过用户-岗位-角色-接口或页面地址这种关系,实现接口级、页面级甚至按钮级的权限控制
  3. 版本(Plan):版本代表不同层级的能力(配额等)。每个租户可以自由购买模块(应用)的不同版本。
  4. 对象存储:我们提供内置的文件上传、图片压缩等能力
  5. 短地址:将过长的 URL 转为较短的地址进行分发