许多内部管理系统在迭代过程中,不可避免的变得越来越庞大,并且由多个团队维护开发,对前端来说以传统的一整块页面站点代码进行开发,显然是笨重且低效的,比如:
收钱吧运营管理系统和许多内部系统一样,拥有复杂、大量的页面,并且不同模块由不同团队维护。为了使各个开发团队独立维护,我们使用了微前端的策略,经过长时间的实践,它也日益成熟。
微前端[1]这个名词,第一次是在2016年底在ThoughtWorks Technology Radar[2]被提出,它将微服务这个被广泛应用在服务端的技术范式扩展到前端领域。现在的前端应用呈现越来越富功能化,而越来越复杂的单体前端应用背后可能是数量庞大的微服务集群,一个团队维护这样的前端项目,它将变得越来越庞大且难以维护,我们将这种前端应用称之为巨石单体应用[3]。
微前端的思想是认为,现代复杂的Web应用一般来说是由多个相对独立的功能模块组合,而这些模块应该是由相互独立的各个团队来维护,这些团队由于分工专业不同,会负责相应特定的业务领域。
收钱吧运营管理平台早期是一个这样的巨石单体应用,它的问题:
我们使用微前端的方案,将运营管理平台这个单体应用拆成松耦合、细粒度的多个系统,并让不同团队独立维护。它的优势:
整合不同业务系统,将不同的管理系统展示在一个容器内,提供统一的菜单入口,所有菜单都按业务模块划分
开发隔离,垂直拆分出了技术平台,各个团队可以独立维护业务系统并接入,减少了开发和上线的重叠
技术隔离,每个微应用能选用各自的技术栈
灵活管理用户权限,SPA可以灵活配置模块和菜单,内部系统只要对接到权限模块就能实现前端功能的可配置化
松耦合,粒度更细,性能更好
平台设计了集开发、注册微服务完整的一套微前端方案,系统架构主要分为:
关系如下图:
运营管理平台主要由容器和主体页面组成,使用传统的iframe[5]技术来组合容器和框架。
容器的结构:
如图所示:
框架系统首先会去它的api服务获取用户信息,保存在全局的内存中,然后调用api获取相应用户权限内的目录和菜单级的资源,渲染Tabs和Menus,登录流程及权限层级会在下边模块详细介绍。
左侧Menu通过菜单级资源渲染,这级资源可以配置域名及路由,点击叶子节点的菜单,主体页就会加载配置的页面。
主体页向容器通信,通过iframe提供的postMessage[6]接口,在容器层面通过addEventListener接口来进行“message”监听:
if ('addEventListener' in window) {
window.addEventListener(
'message',
function(event) {
if (event && /(\.shouqianba\.com$|\.testenv\.com$|localhost)/.test(event.origin)) {
const { data: action = {} } = event;
if (typeof action.type === 'string') {
switch (action.type) {
case 'user/logout':
...
break;
...
}
}
}
},
false,
);
}
框架设置了一些通用的功能,主体页可以通过向容器发送信息来调用:
容器向主体页通信,由于容器向主体页通信大多发生在页面初始加载的时候,所以通过将全局参数encode[7]后再将它加到主体页url上来进行传入。
同理主体页之间的通信,是由主体页向容器发送信息设置全局参数,在加载到通信目标页面时会将参数传入该主体页实现通信。
Janus-cas是基于CAS[8]实现的SSO单点登录系统,由于微前端接入了多个应用,所以登录交由统一的中间服务Janus-cas来管理。
Janus-cas分为两部分:
Janus-cas的流程如下图:
TGC:Ticket-granting cookie,用于记录用户登录信息,存放于浏览器cookie
TGT:Ticket Granting ticket,CAS通过用户名密码生成的票据
ST:Service Ticket,由CAS Client生成传给具体用户,表示CAS服务器授予用户对具体应用的访问权限
资源权限系统分为资源模块和权限模块,资源模块为微前端框架提供了注册页面的方式,主要用于在系统中注册资源,比如目录、菜单等;权限模块则为每个用户提供了前端的权限管理,用于给不同用户分配定义好的资源权限。
资源模块类型:
资源是以树的形式来定义,最上层是目录,下一层是菜单,分别用于定义容器的Tab和目录,菜单可以多层嵌套,以折叠目录的形式来展现;菜单下可以定义子页面或元素,子页面用于定义具体页面的各个模块页面,多用于一些复杂的详情页,元素则用于定义页面上的组件级资源,比如按钮。
资源树结构如:
目录、菜单、子页面都是页面级资源,可以定义具体页面的域名和路由,容器根据域名和路由拼接出具体页面的url。以“目录 > 菜单 > 子页面”的顺序,点击叶子节点的资源会加载相应的页面。
元素是组件级资源,定义了该资源,可以在具体项目中判断是否有该资源权限,从而判断是否显示这个组件。我们提供了一个Authorized组件,使用户更方便使用,具体用法:
// JSX
<Authorized resourceCode="1314">
<button>Delete</button>
</Authorized>
// ts
const hasPermission: boolean = Authorized.check('1314');
权限层级分为:
角色配置资源权限,用户被赋予角色,角色和用户是多对多关系:
以下是资源、角色、用户的关系图:
基于现代越来越富功能化的Web APP,许多前端项目变成了前端巨石单体应用,为了使其能更合理地进行开发和划分,我们尝试了微前端理念。
运营管理平台使用微前端方案以来,已经接入超过500个页面,支持了十几个团队的独立维护,角色配置超过200个,稳定高效地支持了开发团队开发和业务方的使用。这套方案同样适用于其他前端单体应用,当然我们也会持续的优化,比如在页面整合模块上尝试一些其他的方案,通过加载js来渲染主体页,让体验更加的无缝化;增量资源类型,丰富页面的结构等。
希望以此方案与大家共勉,谢谢。
赵志毅,来自增值业务开发部
Micro Frontend: https://micro-frontends.org/
[2]ThoughtWorks Technology Radar: https://www.thoughtworks.com/radar/techniques/micro-frontends
[3]Frontend Monolith: https://www.youtube.com/watch?v=pU1gXA0rfwc&ab_channel=microXchg
[4]SSO: https://www.onelogin.com/learn/how-single-sign-on-works/
[5]iframe: https://developer.mozilla.org/en-US/docs/Web/HTML/Element/iframe
[6]Window.postMessage(): https://developer.mozilla.org/en-US/docs/Web/API/Window/postMessage/
[7]encodeURIComponent(): https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/encodeURIComponent/
[8]CAS Protocol: https://apereo.github.io/cas/6.3.x/protocol/CAS-Protocol.html