前言
献给在中台前端岗位收缩十分严重的今天,依然热爱探索前端技术的前端们,希望大家工作顺心,面试顺利!
本文不涉及具体的实现,是对过去工作的总结, 只讲大体实现思路,适用于任何中台大型项目,实际项目中也涉及到移动端部分,但本文没有提及。
希望可以给大家在微前端体系开发中一些启发,欢迎评论区交流微前端体系使用经验和优化心得,提出补充建议。
项目背景
- 项目组最初由初创时痛点驱动型平台产品到野蛮生长阶段各类业务平台出现如今要往精耕细作发现上发展——对运营一体化工作台有了新需求,需要收敛用户心智,将多个业务平台融合为单一品牌供用户使用。
- 项目组经过多年的迭代,每个项目都有各自的前端技术栈,不同的页面风格和菜单布局,收敛为统一的页面风格迫在眉睫。
- 接入低码页面和体系成本过高,无法推动后端全栈化更快的落地,加快和改善前端开发的效率。
原有单spa应用技术框架对整个项目组业务发展产生了掣肘,2022年中,开始了使用微前端框架对原有多个业务平台进行整合,历时2个月,渐进式的完成了所有项目平台的融合。在微前端体系下,85%的新页面均由后端同学采用low code或者no code模式开发,降低了前端开发门槛和工作量,达到前端降本增效的大目标。
简单了解微前端架构设计的理念
下面是我自己梳理的一些微前端理念需要理解的一些基本概念,多用图片表示
icestark 核心概念
发布流程
路由劫持原理
加载渲染
应用通信
微应用加载渲染
样式隔离方案
我采用 css modules & namespace方案,全局去掉所有子应用的通用样式,全局只引一分全局样式。
脚本隔离
通过proxy 创建 window 快照记录与恢复
// 创建
cosnt sandbo= new Proxy(Object.create(null),{
get(){},
set(){},
has(){},
....
})
const execScript =`with (shadbox) {;${script}\n}`
const execCode= new Function("sandBox",execScript);
execCode(sandBox);
三方隔离
iframe 方案。二次登录问题不友好,双滚动条,尽量不选。
项目微前端落地
框架大图
下面是项目脱敏后的微前端体系落地大图,比较重要的部分后续会做解释,评论区也可提问,后续补充。
微前端主容器的模块设计&主容器性能优化思路&菜单配置的编排
菜单解析加载和子应用加载解析
1. 我为什么将菜单和子容器加载的逻辑抽离出主容器基座?
项目需要管理多个子应用、MPA页面和低码开发的单个页面,同时菜单更新频繁。因此,为了避免频繁打包主容器基座来更新菜单,我需要将菜单和子应用配置抽离出基座,通过接口拉取菜单配置和子应用配置。随着菜单的膨胀,需要配置字段的增多,大JSON已经很难配置和维护了,因此我将菜单和子应用配置可视化,可视化后,产品和后端同学用起来相对得心应手。
菜单结构的设计因团队或公司而异。我的主要思路是将菜单的结构设计为menu,而将子应用的资源配置放到每个menuItem中。通过循环菜单,可以生成框架所需的appConfig资源和入口路径。在icestrak中,将路由全部设置为绝对路径,并且相同的子应用会自动匹配资源。这样的设计可以更加灵活地配置菜单和子应用,同时也方便了框架的使用和维护。
配置菜单字段设计
菜单字段设计依据下面3方面:
- 渲染menu组件的必要属性。
- 生成icestark.appConfig所需属性。
- 实现国际化,鉴权等其他功能的扩展属性。
关于菜单加载和子应用加载性能优化
上面已经初步搭建完成的微前端体系,但当你跑起一个子应用demo的时候,会发现比自己开发spa应用要慢,这里就要说说微前端应用的渲染体系。
当前的渲染顺序
从上图可以看出,子应用资源的加载时机相比于spa应用延后的特别多,渲染就会比传统spa应用慢很多,下面是我的两个优化思路
- 1. 前置预处理子应用资源
在全局大图中,有一个pre-effect的脚本,第三部分就是上面问题的优化思路,前置子应用资源的加载时机,,在加载主容器资源之前就通过逻辑匹配确定子应用的资源,用浏览器的预请求和资源并发机制,提前把首次进入的子应用资源加载好(依据子应用的资源大小提高首屏LCP 200ms~400ms不等)
- 2. 前置菜单接口和处理菜单逻辑
随着接入的子应用越来越多,菜单膨胀到几千行,此时通过接口拉取菜单JSON配置后,再去生成对应的菜单和appConfig耗时可能超过400ms,微前端应用会出现渲染子应用卡白屏的瞬间,为此,和负责开发网关同学沟通,在生成vm模版时,直接注入到window上全局菜单,引入菜单版本概念,菜单解析完成生成的header menu和silder menu 和appConfig 保存到本地indexDb里,window注入的菜单版本和本地不同,运算一遍菜单生成逻辑,更新indexDB里。
大图中分别为
- 3. 前置处理spa子应用的全局初始化接口逻辑
当子应用的资源加载后,初始化子应用项目时,可能会有一些需要通过拉取接口数据后才能初始化完成的子spa应用(比如某些页面需要用户的鉴权状态,某些页面加载前可能需要拉取一些关键接口才能渲染等情况),这个初始化的过程会阻塞子应用的渲染,提取这部分初始化接口逻辑到主容器中,并发的调取首次进入的子应用初始化的所需接口,通过webWorker处理,挂载到对应字应用的全局chidlrenConfig上。(优化结果提高200ms到300ms不等)
主容器模块功能模块设计思路
【公用模块】全局公用 $.request
9个项目历史跨度巨大,所有融合子项目都要做request改造,子应用处于微应用模式下,子应用统一调用公共请求,封装统一错误处理(统一公共组件ui抛出),增加频繁更新接口缓存机制,设计单项目连续错误上报机制。
【公用模块】【性能提升模块】indexDB模块
伪代码如下,做全局信息缓存比如应用的,服务组这种不长更新的,和菜单相关信息缓存
class IndexedDB {
private db: IDBDatabase | null = null;
private storeName: string;
private expires: number | undefined = undefined;// 这里设计的缓存过期时间
private options: {
keyPath: string;
};
private init(): void {
if (!window.indexedDB) {
console.log('当前浏览器不支持IndexedDB');
return null;
}
const request = indexedDB.open('xxxx',version); // 初始化
request.onerror = (event) => {
console.log('打开数据库失败');
};
request.onsuccess = (event) => {
this.db = event.target.result;
};
request.onupgradeneeded = (event) => {
// 创建对应的表
db.createObjectStore(
);
// 创建对应的元数据
db.createObjectStore('metadataStore', {
keyPath: 'storeName',
});
this.db = db;
};
}
public async getData(callback: (data: DataItem[]) => void): void {
// ....
})
public async saveData(callback: (data: DataItem[]) => void): void {
// const transaction 创建 transaction
// 更新对应的表和元数据信息
})
}
【性能优化模块】webWorker 子应用初始化逻辑&接口前置预处理模块
抽离出来的逻辑和接口,根据子应用初始化逻辑复杂度不等,充分利用浏览器并发机制和通过webWorkder计算不占据主容器渲染线程,,首屏LCP提升100ms~400ms。
【容器逻辑】 菜单和子应用配置的生成
....
【公共模块】全局多语言标
主容器里的i18n标是全局唯一i18n标,子应用的i18n标记跟随主容器刷新,并且负责判定子容器加载 zh/en的language语言包,子应用组件国际化跟随容器标记重置。(子应用使用的i18n各不相同,根据全局I18n标识各自强制刷当前页面才可重置成功),这里是否有别的方案,国际化切换时候体感并不是很好,切换国际化后每个子应用都要强制刷新一遍页面,除了使用相同i18nde框架还有别的办法吗?
【公共模块】Errorboundary module 异常捕获,错误屏蔽以及上报
- 前端公共监控SDK的初始化和定制字段上传。
- 上传项目指定自定义错误(比如子应用的白屏等)到自己的服务日志,自定义告警通知,触发钉钉或者电话进行线上告警。
- 子应用错误兜底,避免整个页面白屏,同时触发低级告警。
【公共模块】 全局公共模块
使用 @ice/stark-data设计全局通信体系,初始化一些全局公共接口数据到iceStore里。
【公共模块】系统右侧WidgetSlot插槽模式设计
中台管理系统右侧slot一般有消息通知模块,用户信息模块,全局功能搜索模块和帮助中心模块。 当前主容器基座的设计如果只用在自己的项目中,付出这么多心血其实不值得,如果可以把当前容器设计成企业统一的微前端容器来使用,右侧功能栏的插槽化设计就是最优方案。
这一块实现比较多,需要有动态配置包的能力。 总体思路是将右侧slot抽离成plugin,封装成可插入的plugin插件, vm 初始化时候注入相应的脚本路径和脚本名称,子应用加载后执行插槽的渲染。
【公共模块】主子域名互信token与刷新机制
这个模块是根据我们内部的鉴权系统设计的域名互信机制,当接口请求发起时,主应用和子应用会互相信任当前用户,客户端会根据互信机制生成 token,并写入请求中。根据项目情况,我采用了半小时过期的机制,如果用户在半小时内没有关闭页面,就可以重新拉取互信 token。这个逻辑是在从一个授权的子应用切换到未授权的子应用时触发的。
主子应用域名互信问题
需要设计一种机制,使主域名和子域名之间可以互相信任。这个过程中,一个难点是需要自建网关层,通过服务端代理转发接口的同时,为每一个请求添加 token,以解决接口登录权限和跨域问题。另一个思路是采用免登录授权的方法,使子应用的登录由主应用提供,具体的设计取决于实际情况。因为互信机制是一个复杂的过程,需要考虑多方面的因素,如安全性、效率、可扩展性等,因此需要进行优化和丰富设计。
pre-effect.js的设计初衷
在生成vm模版后可以看到,我在容器加载前强行插入一段脚本,这段脚本执行会阻塞容器js的解析和渲染,但是单独抽离出这么一个前置处理脚本是有必要的,如下图,这个脚本做了3件事。
- 通过iframe与请求互信接口,可以前置首屏加载的子应用互信的时机,减少或者避免子应用已经渲染,但互信没有完成的情况。
- 第二点,在上面菜单优化加载上已经说明,可以明显的提高首屏的加载速度。
- 提前初始化全量变量如环境变量等,让容器里一些依据环境标的逻辑提前运算完成。
iframe 预加载【性能优化方向】
作为中台管理系统,项目本身有很多外部bi报表系统制作的单页面,加载起来十分慢,通过display:none抽屉来预加载其中的iframe内容,能加载vm上的js和css,很多font和png只有display:block的时候才会再去加载,中间还是会有白屏一段时间。
子应用相关改造和性能优化
spa子应用改造
在微前端体系改造中,子应用的改造是工作量最大和最复杂的部分。这是因为一些旧的构建工具已经过时,无法满足改造的需求,一些依赖的包也是基于 Node 8 开发的,无法升级到最新版本,这给升级工作带来了很大的困难。因此,升级的过程变得费时费力。需要采取措施来解决这些问题,如引入新的构建工具、逐步更新依赖的包等,以确保子应用能够顺利地进行工程化改造。同时,需要认识到这是一个长期的过程,需要耐心和不断地优化和迭代。
总体思路:保留原本老站点,使用一套代码,增加umd打包格式。工程改造为微前端启动和原有端形式启动,线上资源部署会分为spa打包模式和微前端打包模式,分别独有资源版本号。
子应用改造如何融入微前端体系没有标准的方案,很多策略依据自己的项目情况而定。
难点
简单列举一些子应用改造中的难点
- 项目很多很杂,构建工具有webpack,还有内部基于webpack封装的自己的构建工具,也有vite项目和gulp项目的老项目,构建配置各不相同,统一改造成umd构建产物的难度不同。
- 分包后,多个webpack版本会有可能让publicPath相互污染,拼出分包的资源的cdn全路径的确定因项目而定。
- 在微应用的改造中,子应用路由的统一 basepath 应该在菜单编辑时就规划好,以确保所有的子应用都能够正确地加载路由和资源。为了避免 basepath 的冲突,设计时考虑到不同的子应用之间的区别,并且在菜单编辑时进行明确的规划和设置。
- 改造微应用后,是否保持原有spa应用平台可用,采用版本控制、构建工具的配置等方式来管理资源和构建产物,确保不同版本的资源可以独立存在,并且能够正确地加载和使用。
- 防止样式的相互污染,尽量把相同的组件库都抽离成一个全局脚本再引入,无法统一引入的也需要自定义prefix前缀,同时,要将子应用中的全局样式文件移除,统一由主容器引入。 下面为伪代码
{
"build:mfa": "REACT_APP_ENV=mfa wepback build",
"build":"wepback build",
"start:mfa": "REACT_APP_ENV=mfa wepback start",
"start:mfa": "wepback start",
}
线上构建拿取环境信息中的version注入到编译环境中,确定完整cdn路径
command echo $BUILD_ARGV_STR | sed \"s/.*--version=\\(\\S\\+\\).*/\\1/g\"
- 无法改造成子应用的就需要使用最老的iframe方案了,使用postMessage使iframe子应用的和容器的的url同步更新。
spa子应用性能优化
大致说一些优化思路和方向
- 升级子应用依赖的webpack的版本到5.0以上版本,通过webpack 5.0的tree shake机制,采用esbuld/terser等压缩方式,过滤掉无用的代码块和依赖,拆分弹窗等后置操作为懒加载,脚本的按需引入,优化代码逻辑,减少重复渲,大数据量数据处理通过webWorker 处理等方式减少首包体积,加快子应用的解析提升LCP。
- 通过微前端主容器,对子应用资源做缓存。
- 对css不合理的地方就行调整,减少屏幕的抖动,尽量减少整个页面的重绘行为。
- memory分析内存的占用情况,找到一些内存泄露的点,来减少浏览器的负载。
- 提取几个子应用的公共依赖,某一个子应用加载公共依赖后,后续切换到的子应用就可以走本地缓存,减少其他子应用的首包体积。
- 静默刷新的能力
- 尝试统一子应用的字体
新页面
微前端体系让mpa应用重新占据优势,新页面都通过mpa应用打包生成单页面引入微前端体系。