查看原文
其他

【第3143期】如何提升微前端场景下的研发效能?微前端管理平台的设计与实践

秋池 前端早读课 2024-03-11

前言

介绍了微前端场景下如何提升研发效能,并通过设计和实践微前端管理平台来解决应用管理、权限管理和菜单编排等问题。通过集中处理数据和提供治理能力,可以降低开发成本,提升用户体验,赋能其他业务,以及实现未来能力的扩展。今日前端早读课文章由网易 @秋池授权分享。

正文从这开始~~

术语说明

  • 子系统:产品内部的应用,一般有自己的代码仓库,且业务相对独立,如网易七鱼内部的呼叫系统、在线系统、工单系统等。

  • 主应用:微前端应用的主体框架,负责子应用的注册、配置下发、加载和切换等。也被称为基座应用。

  • 子应用:微前端应用的主体内容,属于特殊的子系统,一般与主应用协同运行。

项目背景

网易七鱼是网易旗下的一款 SaaS 模式的云客服产品,致力于通过自主研发的客服机器人为企业客户降低企业管理成本,提高客户满意度,随时随地解决客户问题。

同步数据与权限

网易七鱼的应用整体采用 Java + FreeMarker + React/NEJ 的方案落地。前端的渲染依赖后端注入到 FreeMarker 页面模板中的同步数据(同步数据中包含企业权限、角色权限、个人信息等),前端拿到同步数据后需要对它们做进一步处理,如数据格式化、默认值兜底等。

因为每个子系统都需要维护一套自己的同步数据(虽然有的业务场景微前端子应用使用主应用下发的同步数据,但这些同步数据的增删改归根结底还是需要自己在主应用维护),导致同步数据分散在各处维护困难,主应用与子应用数据冗余,改动时的影响范围大、测试成本高等问题。

应用的维护成本

一个主应用下可以注册多个子应用,子应用的注册信息、部分权限数据等需要在主应用中手动维护。因此每次修改子应用的这些信息时,都需要对主应用进行修改、构建、发布、测试,导致相关需求交付效率低。如何不侵入代码就能实现注册 / 注销子应用、维护子应用的信息?如何高效控制应用所需的权限?随着应用数量的增多,如何了解每个主应用下注册了哪些子应用,应用间的关联关系是什么?

跨业务域的应用组合

目前网易七鱼的一个子系统对应一个应用,而这个应用可能包含不同业务域的业务,业务需求涉及到跨组协作,如跨组提 MR、代码合并冲突检查、CodeReview、协调不同组的发版计划等,增加了维护成本和沟通成本。例如网易七鱼下的坐席辅助子系统,被当做一个 SKU 售卖,这个子系统有在线相关辅助和呼叫相关辅助组成,这两个业务域分别有不同业务组的同学负责开发和需求迭代,把它们耦合到同一个子系统中就会出现上述问题。如何降低以上成本?最好可以把应用按不同业务域解耦,各个业务组对自己业务域的需求负责。那怎样才能做到把多个不同业务域的应用组合成一个子系统呢?(因 iframe 方案缺点较多,本文不考虑此方案)

总体设计与思考

将应用的数据层(应用基本信息、关联关系和权限数据)与 UI 层分离,在平台层集中处理这些数据。前端应用所需的数据全部通过接口获取,去除 FreeMarker 同步数据层。应用的基本信息、关联关系、菜单权限等均可在平台层维护和查看。

平台层支持前端应用的接入,为接入的应用提供中心化的治理能力。那么它能解决当前业务中的哪些问题?能给整体的业务带来哪些价值?

解决的业务问题

应用管理

  • 应用的注册 / 注销成本大,需要重新修改、构建、发布和测试主应用

  • 应用没有统一管理的地方,不能方便的查看和操作,包括管理应用的基本信息(路由、分组、代码仓库等)、应用间的关联关系、负责人等

  • 跨业务域的组合的应用维护成本高

权限管理

  • 前端应用依赖由后端通过 FreeMarker 注入的同步数据,导致无法做彻底的前后端分离、前端静态化部署等优化

  • 应用的权限使用不规范,主应用和子应用会分别从后端同步数据获取权限,会有部分数据冗余,数据量大时会导致页面加载时间长

菜单编排 一般菜单的展示受权限控制,需要将菜单与权限数据共同管理。而且在应用组合的场景下,需要解决菜单数据冗余、维护成本大的问题。

带来的业务价值
  • 提升研发效能:针对应用信息变更、菜单编排、权限控制等需求,可以减少资源投入。为将来的前后端彻底解耦、静态化部署等打下基础

  • 提升用户体验:子应用渲染不再需要等待同步数据,可以只请求所需的数据,页面加载时间变短

  • 赋能部门其他业务:为保证其他业务低成本接入平台,需要平台支持 API 配置,以获取接入业务的权限数据,接入方只需要实现自己的权限数据接口即可使用

  • 未来能力扩展:基于平台还可以做应用的健康检查、发布 / 回滚、日志、资源管理等

实践方案

上文介绍了项目背景和设计思路,这里主要讲具体的设计与实现方案。主要包括以下几个方面:

  • 产品管理

  • 应用管理

  • 菜单编排与权限配置

架构图

微前端管理平台只能通过内网访问,外部用户无法直接访问。这样设计的好处是,用户请求的流量不会走到微前端管理平台的服务,因此不需要把它做成 P0 服务,维护成本和风险都比较低。

整体流程
  • 开发者通过管理平台配置应用信息、关联关系、菜单编排数据与菜单权限

  • 主应用通过后端接口获取平台下发的配置数据(包含应用信息、菜单编排及菜单权限等),以及与当前企业相关的权限数据,然后通过解析企业权限数据获取菜单权限的值

  • 主应用将解析后的数据下发给子应用,下发数据的方式有:

  • 将数据挂到全局变量上,这种方式对子应用的侵入最小,方便接入和调试,但需要添加命名空间,降低被污染的风险

  • 通过 props 传递,这种方式最安全,但对子应用的侵入较大,尤其是 utils 类的代码

  • 子应用拿到数据后走正常的渲染流程

产品管理

产品管理比较简单,主要是满足以下特性:

  • 一个产品内可以包含一个或多个应用

  • 不同产品之间的数据相互隔离,对应用设置前必须先选择一个产品

  • 每个产品需要支持设置业务分组,以标识业务与组织的对应关系

如何支持其他业务产品的接入?

  • 添加产品时,支持相关权限数据接口对接

  • 微前端管理平台的后端服务提供 openAPI,供接入方获取配置数据

  • 接入方的产品弱依赖微前端管理平台的后端服务,读取数据后记到缓存,定时刷新

  • 后端服务需支持鉴权功能、熔断策略

应用管理

应用管理包括主应用及子应用的添加 / 删除、关联关系、应用分组、负责人、创建时间等。应用的增删就是常规的 CURD,需要注意的是对关键数据的操作最好做二次弹窗提醒。应用的基本信息 应用信息需要包含应用名、基础路由(一般为应用内各页面的公共前缀)、仓库地址、创建人、维护人、所属组(添加产品时设置的业务分组)、应用简介等。下图为主应用的基本信息配置:

子应用的基础信息在主应用的基础上增加了应用入口、容器选择器这两个字段,应用入口是子应用的 HTML entry,容器选择器为子应用在主应用中渲染的位置。

主应用与子应用的关联 一个主应用可以关联多个子应用,一个子应用也可以关联多个主应用。子应用关联主应用后就会被注册到该主应用,不能重复注册。因此主应用下关联的子应用必须是唯一的,反过来也一样,我们以基础路由来区分各个应用。新增应用关联关系这里在设计时有一个需要考虑的问题,这个新增的操作是放到主应用侧还是子应用侧?还是两边都放?我们知道主应用关联子应用后,意味着子应用会马上被注册到主应用,此时如果子应用的信息不完成或者填写错误,到线上就可能出现异常,而在主应用侧校验子应用的信息比较麻烦。相比在子应用侧添加关联主应用时,主应用基本不会出现异常,子应用的基本信息也可以方便查看,所以我们选择把新增关联关系的操作放在子应用侧。主应用详情页:

子应用详情页:

子应用间的关联 / 组合

因为关联的子应用也会被注册到主应用,所以同一个主应用下关联的子应用必须是唯一的。子应用关联时的列表:

子应用关联后的详情

相互关联的子应用会复用关联的主应用、菜单编排与权限设置等数据。它们的唯一区别是基础信息不同。

菜单编排与权限设置

菜单的展示一般会受到权限控制,因此菜单与权限有比较强的耦合关系。因为平台层是无法获取具体企业信息的,所以我们在平台配置的菜单权限是与企业无关的,具体来说,我们配置的只是一个获取权限值的表达式,而不是计算好的权限值。这就后端接口提供的是当前业务下完整的权限字段(前端只读就可以),平台不需要关心权限字段对应的 value。菜单编排 菜单项支持新增、删除,以及通过拖拽调整层级和顺序。问题来了,子应用的菜单编排放到主应用内还是子应用内?应该放到子应用,因为子应用可能关联多个主应用,子应用自己的菜单编排内聚到应用内部更方便维护和迁移。

除手动编排菜单项外,还支持复制和导入菜单数据,以便快速生成从其他复制的菜单数据:

编排完成后,也支持复制出 mock 数据方便在开发环境使用。权限设置 权限设置这里在设计时有一个需要考虑的问题,是采用用户自由输入的配置方式还是可视化的配置方式?这里采用的是可视化的配置方式,原因如下:

  • 便于对配置数据做正确性校验,避免配置出错

  • 可以通过工具约束用户的输入,提高可维护性

  • 解析时可以将获取权限的代码的执行上下文限制在权限数据对象内,没有潜在的安全性问题

要实现权限数据的可视化配置,首先需要设计一套满足需求的 JSON schema,用它来表达对应的权限逻辑。JSON schema 对应的 Typescipt 声明如下:

import type {MergeExclusive} from "type-fest";

// eq等于、ne不等于、gt大于、lt小于、ge大于等于、le小于等于
type valueOperator = 'eq' | 'ne' | 'gt' | 'lt' | 'ge' | 'le';
type boolOperator = 'falsy' | 'truthy';

// 当前条件与同级条件的关系:and为同时满足,or为满足任一
// empty 空节点, 覆盖(setting.b || setting.c > 2) && (!setting.a || setting.d)的情况
type relationType = 'and' | 'or' | 'empty';

interface IPermNoConditionNoValue {
type?: relationType;
expr: string; // 表达式,例如'setting.a'
operator?: boolOperator; // 默认值是真值truthy
}

interface IPermNoConditionWithValue {
type?: relationType;
expr: string;
operator: valueOperator;
value: string;
}

// 处理数组的包含关系
interface IPermNoConditionArrayIncl {
type?: relationType;
expr: string;
arrOperator: 'incl' | 'excl'; // incl包括、excl不包括
value: string;
}

// 处理数组的长度
interface IPermNoConditionArrayLen {
type?: relationType;
expr: string;
arrOperator: 'len'; // len长度
operator: valueOperator;
value: string;
}

/**
* condition 目前最多 4 层
*/

interface IPermWithCondition {
type?: relationType; // 描述当前节点与上一个相邻节点的逻辑关系。对首个根节点或首个condition节点禁止选择type,对其他节点必须选择type。
operator?: boolOperator; // 表达式的操作符,默认值是真值「truthy」
condition: IPermission[]; // condition与父级节点的逻辑关系是「and」
}

// 菜单项的权限
export type IPermission = MergeExclusive<MergeExclusive<
MergeExclusive<IPermWithCondition, IPermNoConditionWithValue>,
MergeExclusive<IPermNoConditionArrayIncl, IPermNoConditionArrayLen>
>, IPermNoConditionNoValue>;

需要注意的是,不同的配置方式会产生不同的数据结构,表达式和数据结构是一对多的关系。如表达式:(setting.b || setting.c > 2) && (!setting.a || setting.d) 对应的 JSON schema 可以是:

[
{
expr: 'setting.a',
operator: 'falsy',
condition: [
{
expr: 'setting.b',
operator: 'truthy'
},
{
type: 'or',
expr: 'setting.c',
operator: 'gt',
value: 2,
},
]
},
]

也可以是:

[
{
expr: 'setting.a',
operator: 'falsy',
},
{
type: 'and',
condition: [
{
expr: 'setting.b',
operator: 'truthy'
},
{
type: 'or',
expr: 'setting.c',
operator: 'gt',
value: 2,
},
]
},
]

配置菜单项时的 UI 如下:

新增权限项:

配置菜单所需的权限字段是从接口获取的,字段可能较多,为了提升配置效率,需要支持展示完整权限数据,并提供关键字搜索,UI 如下:

降级方案

微前端管理平台相关服务对用户是无感知的,用户侧的请求流量也无法直接走到平台侧。因此应用接入微前端管理的风险点主要在于以下接口的可靠性:

  • 接口 1:获取平台侧下发的配置数据

  • 接口 2:获取企业相关权限数据

整两个接口正常情况下都是由主应用调用,走 http 请求。当接口 1 挂掉时,主应用无法获取平台下发配置,也就是无法执行最基本的应用注册流程,此时的降级方案是使用主应用内的应用信息兜底数据,保障核心服务的注册流程能跑通。当接口 2 挂掉时,主应用无法获取当前企业的权限数据,相当于此前的 FreeMarker 内同步数据异常,页面内所有依赖权限的数据都可能是错误的,此时的降级方案是使用兜底的权限数据默认值,避免出现页面白屏等严重影响用户体验的问题。

环境隔离

平台需要支持的环境包括回归、预发、线上(区分灰度和全量)。回归、预发、线上可以通过域名来区分。线上的灰度和全量通过新建产品的属性来区分,好处是不需要再搞一套域名,添加产品时后端只需要加一个字段,实现成本较低。切换环境时自动在产品名称后加对应的后缀以作标识。

此外,不同环境使用水印提醒,以免对数据的误操作。水印由环境名及用户名组成。

其他工作
  • 成员管理:支持角色权限,包括普通成员、管理员、超管。需要限制管理员及以上角色才能执行重要的操作,如果删除应用、解除应用关联关系等

  • 操作日志:记录关键操作,如登录 / 登出、数据的增删改等

  • 内网 openID 登录

  • 网关路由配置

  • 快速上手文档

效果展示

总结

本文从满足网易七鱼需求的单点出发,介绍了从 0 到 1 构建微前端管理平台的过程。从需求出发反向思考如何应对未来业务的变化,使系统架构更加符合现有的组织架构(康威定律),从而提升研发效能。

【第3121期】效率前端微应用推进之微前端研发提效

一般一个系统的实现会有多种技术方案,而系统设计就是围绕这些实现方案不断作权衡和取舍的过程,中间需要考虑各种因素,如可行性、安全性、实现成本、可维护性、可扩展性等,最终将方案收敛到一个 ROI 最高的版本。每种技术方案都是既有利又有弊,适合业务的才是最好的。

关于本文
作者:@秋池
原文:https://zhuanlan.zhihu.com/p/586148353

这期前端早读课
对你有帮助,帮” 
 “一下,
期待下一期,帮”
 在看” 一下 。

继续滑动看下一个

【第3143期】如何提升微前端场景下的研发效能?微前端管理平台的设计与实践

向上滑动看下一个

您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存