查看原文
其他

中后台平台化探索和演进

顾闻佳韩阳孟真 哔哩哔哩技术 2024-03-11

本期作者



顾闻佳

哔哩哔哩资深开发工程师


韩阳

哔哩哔哩高级开发工程师




孟真

哔哩哔哩高级开发工程师


1. 前言


公司在经过多年的发展,部门架构的演进,产生了大量的管理后台,包括运营平台、技术后台等。有持续迭代的,有年久失修的,也存在大量无人认领和维护的。

从技术层面看,大量的后台都存在基础能力缺失,或者重复建设,同时技术架构也各不相同。

在我们团队维护的后台里,很容易总结出如下共性问题,“技术栈不统一、规范不统一、重复建设”



2. 背景分析


目前我们维护着B端大约30多个管理后台,累计超过300个模块和1400个页面,随着业务的不断融合,和新业务场景的加入,平台数量还在逐步上升中,从工作过程中我们看到了如下问题:

  1. 系统重复建设、迭代和维护成本高

    a. 这里的重复建设不仅是业务功能模块的重复建设,在开发每个后台系统时,还存在大量的基础建设的开发,比如常见的身份鉴权和操作日志,这些都是重复建设和存在重复维护的

  2. 相互独立,难以技术演进

    a. 由于每个后台系统存在于不同仓库开发和维护,无法实现统一的技术改造和通用能力建设

  3. 用户体验不统一,使用成本高

    a. 太多的后台入口,各种不同的权限申请流程,我们的日常工作需要在多个后台间切换来完成



3. 目标规划


针对以上的问题,我们从2021年开始着手如何统一后台业务入口,降低部门内后台的维护成本。当时事业部内后台数量大约10个左右,但是已经可以预见到的是每个团队都在构建不同场景的后台项目,为了解决业务口径和资源效率问题,减少将要发生的历史债务,我们启动了中后台整合的微前端项目,并规划了以下几个技术目标:

  1. 所有后台入口统一,体验统一

  2. 统一平台基础能力,减少重复建设

  3. 对全部业务的可观测能力建设,了解业务资产和债务



4. 整体设计


为了完成以上3个技术目标,我们开始了微前端的技术调研工作


关于微前端


目前,业界已经有许多微前端的解决方案,我们前期主要调研了比较流行的qiankun和micro-app,这里简单分享下我们使用下来在体感上的区别

qiankun - 基于single-spa,但提供了更多开箱即用的API

样式隔离:

ShadowDOM
⚠️ 在开启后,实际体验比较差,子应用的原本样式 引用官方原话,”基于 ShadowDOM 的严格样式隔离并不是一个可以无脑使用的方案,大部分情况下都需要接入应用做一些适配后才能正常在 ShadowDOM 中运行起来”

所以官方后期还提供了第二种解决方案,为子应用样式增加特殊的选择器


// 假设应用名是 react16.app-main {  font-size: 14px;} // 开启隔离后div[data-qiankun-react16] .app-main {  font-size: 14px;}


子应用接入成本:

高,要求子应用修改渲染逻辑并暴露生命周期,和对打包配置要进行一些修改

micro-app - 将子应用封装成一个类WebComponent组件

样式隔离:

CSS Module
兼容性高,我们的一方应用(团队内后台)和部分二方应用(其他团队后台)都能在开启样式隔离后无缝接入


// 开启前.ant-table table {    width: 100%;} // 开启后micro-app[name=app_name] .ant-table table {    width: 100%;}


子应用接入成本:

最后两者都配备了JS沙箱,micro-app基于Proxy,qiankun则更复杂些,我们主要体验了micro-app,在有大量dom操作时会存在些性能问题,主要是在创建和销毁大量dom时。


微前端选型


所谓“没有最好的技术,只有最适合的”。

再来分析我们业务场景,首先我们有着许多的存量业务,并且这些存量业务需要持续迭代。我们作为业务交付团队,没有过多精力放在老后台的接入改造或是重构迁移上,所以我们在方案选型上会考虑:

  1. 存量后台要尽可能的无损接入,无改造成本,保持原部署方式和访问路径

  2. 应用要保持样式隔离

  3. 提供平台化能力,或开放能力,减少重复建设

  4. 路由中心化管理(这也是我们后期完成资产统计和全站搜索的必备条件)

  5. 统一业务接口网关,方便实现日志监控、身份鉴权

最后我们选择了对子应用代码侵入最小的micro-app作为我们的微前端选型


为什么不考虑自研?首先一个成熟的框架是需要持续性投入的,我们团队本身承载的是业务诉求,另外团队变动也存在很大风险,所以借鉴社区方案,尽早上线MVP版本,能讲故事、给大家建立信心和发挥想象力,最后再去做定制化开发才是最好的解决方案


但是,micro-app仅提供了微前端的解决方案,想做好一个平台化的产品,除了微前端外还有许多其他的技术难点需要解决。


 源宇宙工作台


我们把整个项目命名为源宇宙工作台,命名除了蹭当时元宇宙热点外,还有个slogan是把源自各团队的中后台统一管理和使用。(一个好项目必须从名字卷起2333)

先分享一个我们整体架构图,在源宇宙里我们会把每个第一方和第二方后台抽象成一个App应用。



流程大致分为:

  1. 用户在打开我们平台的应用时,首先会进入到我们的路由管理,在路由管理里我们会有一些中心化的埋点和权限管理

  2. 然后进入到有<micro-app>的基座页面,通过路由分发打开各自的子应用里的页面

  3. 最后我们在基座会对所有后台的xhr请求进行拦截,将请求走向我们的nodejs网关,统一了服务端接口口径,也解决了子应用下各自域名不同所产生的接口跨域问题。


存量业务接入


应用的接入是非常简单的,就如上面所说的,micro-app提供了对子应用最小的代码侵入,我们只需在子应用的路由文件里适配下基座应用的uri,这也符合我们一开始的技术目标


const router = createRouter({  history: createWebHistory((window.__MICRO_APP_BASE_ROUTE__ || '') + '/'),  routes: getRoutes(),}) // 在基座应用下window.__MICRO_APP_PROXY_WINDOW__能读取子应用里的windowconsole.log(window.__MICRO_APP_PROXY_WINDOW__.__MICRO_APP_BASE_ROUTE__)// 打印'/live-app/bchat'


这里,当通过基座应用去加载子应用,代码里就会是createWebHistory(’/live-app/bchat’)

比如完整url https://console.bilibili.co/live-app/bchat/member

  • live-app是我们的基座应用路由

  • bchat是我们的子应用代号,通过我们的配置后台,会访问到子应用的真实地址上customerservice.bilibili.co

  • member则是子应用路由

考虑到大家的收藏夹里会保留以前域名的页面地址,为了兼容老地址正常使用,还做了一层跳转,兼容老地址不会失效。


<% if(NODE_ENV === 'production'){ %> <script>     if(!window.__MICRO_APP_BASE_APPLICATION__){      window.location.href = '//'console.bilibili.co/live-app/bchat' + window.location.hash + window.location.search     } </script><% } %>


以下是平台管理内的部分功能截图:



这样一个应用就接入完成了,应用在接入源宇宙后的呈现方式



过程中还会遇到css样式隔离,弹窗层级,静态资源路径等问题,但由于要处理的都是一方和二方的应用,所以处理起来不难。同时我们也会有一些灰度策略来渐进式的接入应用,比如弹窗通知,让用户可选择的跳转到新平台使用,最后后台业务在问题发生后的压力上相比C端业务会少上许多。


 接口跨域


跨域是我们在接入应用时遇到最多的问题,当我们的主域console.bilibili.co去fetch子应用的域名时就会发生跨域,简单跨域可以基于我们的SLB平台为域名添加通用跨域头解决,也就是配置nginx的Access-Control-Allow-Origin,但部分子应用里的业务接口也会发生复杂跨域,比如:

  1. content-type为application/json

  2. 存在自定义header头

  3. method为PUT、DELETE时

当发生复杂跨域时,浏览器会进行OPTIONS预请求,常规的处理方式是服务端接收这个OPTIONS请求并进行回复,但这样会产生服务端的改造,会把每个子应用的接入成本升高。面对这个问题我们的解决方案是后台nodejs网关

首先,我们在应用接入的配置项里加了个开启项



开启后我们会拦截应用内的XHR通讯,经过一些处理后转发到我们的nodejs网关里



子应用内的XHR拦截主要通过ajax-hook库来实现,思路如下


import { proxy, unProxy } from "ajax-hook" microApp.start({  lifeCycles: {    beforemount(app) {      proxy({        onRequest(config, handler) {                    // 补全子应用接口的域名,因为拦截到的url可能是相对路径                    if((!/^f(ile|tp):\\/\\//.test(reqUrl))) {                config.url = createURL(config.url, addProtocol(appConfig?.url)).toString()              }                    if(appConfig?.is_proxy_intercept) {                        // 如果开启了接口拦截,则拦截至网关              handler.next({                        url: '/nb/mapi/proxy/call',                        method: 'POST',                        headers: {                          'content-type': 'application/x-www-form-urlencoded',                        },                            body: qs.stringify({                                real_url: config.url,                                method: config.method,                                params: config.body,                            })                        })                    } else {                        handler.next(config)                    }        },      })    },  },})


通常情况下,我们对于GET请求只做地址补全,但也会有些极端的情况需要走网关,所以我们通过一个强制拦截白名单来控制。另外还会有类似文件下载,文件上传等场景,我们通过接口的content-type来判断走向不同的代理接口,如upCall和txtCall

对于网关层的主要功能是透传,但是我们还添加了诸如日志、trace、告警等通用能力。


5. 平台化探索


在完成mvp版本之后,我们将自己团队内所有平台进行了接入,为接入配置进行了抽象化设计,使得每个应用在接入时成本降的很低且具备可复制性。

当方案成熟后,就到了发挥想象力的时候,我们开始了平台化能力的建设,这也是在微前端演进之后给我们带来的实际价值,因为平台建设可以服务于所有被接入的后台。


路由信息上报


通过@datachange数据通讯,子应用可将路由实例提供给基座应用,由基座应用完成路由信息上报,并且自动更新,这也是我们能做到中心化路由管理的先决条件。



全局搜索


由于收集到了所有平台的路由信息,那就可以很方便的做出全站搜索,用户可以搜索到所有平台下的相关页面,然后在我们源宇宙工作台内打开,方便快捷。



 Tab标签卡


用户每打开一个新的页面,都会在Tab栏里记录,之后就可以在这里快速打开要访问的页面,同时也支持把常用页面置顶,和拖拽排序。



实时反馈系统


通过在主框架里接入我们的反馈工具,即可给所有业务平台带来服务,便于用户进行问题上报和问题定位。



中心化权限管理


每个应用还可以在平台配置中选择是否需要进行可访问权限的控制,和白名单人群。因为所有的访问入口,都会收拢到console.bilibili.co/live-app/路由内,我们只需要在此去判断用户的访问权限即可。

这种设计可以满足一些比较简单和未接入权限系统的应用使用。



错误监听


由于统一了底层框架,我们通过在全局注入对Error事件的监听来达到错误日志的上报和告警。


. 资产与债务的探索


以往,我们只知道开发了多少后台,但从不知道这些后台共有多少页面,和哪些页面已经成了遗留系统已经无人使用。

或者因组织架构变动,在被交接其他后台时他的后台状况是怎样,有多少页面、有多少用户和代码复杂度如何,从而分析交接会给团队增加多少维护成本。

在平台化功能建设里,由于我们捕获了所有应用的路由信息,再通过基座应用来监听URL的变化,实现了全局的数据埋点,最后根据页面总量和平台pv数据,就可统计到我们想要的后台资产和债务。



在了解到整体资产数据后,我们会去整理哪些业务已经是遗留系统,针对遗留系统我们制定了一套下线机制。目的是在减少日益增多的冗余业务的同时,也能让业务团队了解他们在我们平台上的资产与债务。也可以为以后在后台业务的精细化运营上做好准备工作。



7. 业务收益


我们通过微前端方案在最小的人力投入下解决了后台口径收拢和平台化赋能的演进方案,但是,想要做到全公司平台的体验统一、基建统一,或是解决重复建设还需要付出较大的工程投入。例如,每个后台都还存在不同的权限系统,相同业务组件的重复开发。

做减法将是我们下一步的目标规划,通过跨平台的业务组件库来减少每个系统里的重复建设,和规范各种质量与体验的标准。

最后,再来总结下一开始我们设定的技术目标和平台的实际价值:

  1. 通过我们的源宇宙平台,我们实现了所有后台应用的体验统一

  2. 在基建能力上我们可以从平台出发实现全站注入,减少了重复建设

  3. 面对不断增加的业务输入,我们可以从顶层观察公司内的平台状况,发现无效建设和重复建设

  4. 最后可以设定成本,实现面向平台化的精细化运营


以上是今天的分享内容,如果你有什么想法或疑问,欢迎大家在留言区与我们互动,如果喜欢本期内容的话,欢迎点个“在看”吧!


往期精彩指路

继续滑动看下一个

中后台平台化探索和演进

顾闻佳韩阳孟真 哔哩哔哩技术
向上滑动看下一个

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

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