居然有快半年没更新了... 最近对微前端深入了解(加班)比较多,所以想陆续输出一些围绕微前端的小想法💡。
微前端
微前端是什么就不赘述了,不了解的小伙伴可以看下 qiankun 的官网。一句话说,是通过 JS 沙盒保证子应用“一定程度的”与宿主隔离,同时对前端运行时框架进行兼容,从而便捷地在不同宿主间复用。
一般聊到微前端,基本是因为一坨大屎山(激进了,客观的说是巨石应用)的复杂度熵增到,维护的心智在业务快速迭代中无法被忽视了。那么我们选择,让子应用使用微前端的分治思想进行迭代:独立开发和部署、互相隔离、框架无关,从而解决高度复杂的单体应用迭代难、维护难的问题。
错误上报
前端工程化中少不了监控告警,对于微前端也是一样,我们需要错误上报、监控告警的能力。但是对微前端来说,区分错误“真实来源”并不是那么简单。主、子应用间混合上报,或子、子应用互相污染上报,源码定位失效,在茫茫监控数据中,手动 ignore 和 filter 的成本太高。
业界监控大佬 Sentry 确实给出了微前端上报“自动路由”的具体操作方案,但具有一定的局限性。我们简单看下官网的代码🌰, 了解基础原理。
子应用打包时填入 meta:
// webpack.config.js
const { sentryWebpackPlugin } = require('@sentry/webpack-plugin')
module.exports = {
devtool: 'source-map',
plugins: [
sentryWebpackPlugin({
/* Other plugin config */
_experiments: {
moduleMetadata: ({ release }) => ({ dsn: '__MODULE_DSN__', release }),
},
}),
],
}
宿主侧运行时根据 meta 判断来源:
const EXTRA_KEY = 'ROUTE_TO'
// 省略一部分 transport 代码...
init({
beforeSend: (event) => {
if (event?.exception?.values?.[0].stacktrace.frames) {
const frames = event.exception.values[0].stacktrace.frames
// Find the last frame with module metadata containing a DSN
const routeTo = frames
.filter((frame) => frame.module_metadata && frame.module_metadata.dsn)
.map((v) => v.module_metadata)
.slice(-1) // using top frame only - you may want to customize this according to your needs
if (routeTo.length) {
event.extra = {
...event.extra,
[EXTRA_KEY]: routeTo,
}
}
}
return event
},
})
那么问题来了,有没有觉得心智负担陡增?
- 子应用编译时、宿主运行时都需要对 meta 进行操作,一旦有变动这在跨部门协作中是比较蛋疼的。
- 当一个生产者导出若干个微模块,而这些微模块有的希望是独立的监控,有些是希望聚合查看、同时支持筛选的,那么对于微前端的监控配置将非常复杂。
- 有些公司自己的监控轮子,可能根本不支持类似 sentry 的微前端 auto routing 能力。好了,那新的造轮子大业又要开始了。
所以对于一个懂事的微前端框架,是不是可以自己解决这套逻辑?
错误上报的隔离分发思路
从上面 sentry 的例子不难看出,整体的思想是,为每个子应用增加一个标识,这个标识将会在宿主运行时被消费。通过分辨标识的来源,宿主会将错误分发到不同的监控实例(或 id 等任意监控控制面可切换的维度)中去。所以如果我们想自己在微前端框架中实现,那么有两个绕不开的关键点:
- 子应用的标识
- 错误分发
通常来说,第一点“子应用的标识”对于微前端属于“天然存在的”。在微前端框架设计中,一个很突出的点是沙盒
。可以是通过 Proxy, 也可以是任何别的技术实现的沙盒。沙盒就意味着隔离、互不干扰,那么对于每个子应用一定有标识
存在,不管是通过运行时还是编译时注入。那么对于错误隔离来说,也是类似的。那么就只需要解决第二个问题 —— 错误分发。
错误分发
错误监控的原理是:对 window 的 error
和 unhandledrejection
进行监听、处理和上报。所以微前端运行时框架要做的,就是最先注册监听事件,并设置在捕获
阶段进行处理。之所以强调捕获
, 是为了更早地捕获事件,为父级元素赋予处理事件的能力,特别是在微前端错误监控场景下,便于在宿主侧进行更细粒度的控制。
摸清思路后,接下来是一段伪代码。需要注意一点:在运行框架自身的错误分发逻辑后,要阻止错误事件 fire 其他监听器,避免功亏一篑。
const getRoute = (e) => {
// 根据错误堆栈,拿到错误路径
}
const listener = (e) => {
let route = ''
if (e.type === 'error') {
route = getRoute(e.error)
} else {
route = getRoute(e.reason)
}
// 判断触发的路径是否符合 sandbox 中的一些路径条件
if (route.match(/.../)) {
// 执行沙盒中对错误事件的回调,
sandbox.errorHandler(e)
/**
* @see 关键!
* 阻止触发其他任何在相同元素注册的 error 或 unhandledrejection 事件
* 只有加上这行,才能保证其他监控 SDK 的上报不会走到其他错误的微前端模块或宿主
*/
e.stopImmediatePropagation()
}
}
root.addEventListener('error', listener, { capture: true })
root.addEventListener('unhandledrejection', listener, { capture: true })
上面的代码只是比较粗略的框架,需要大家细心填补,微前端框架百花齐放,这段也只起一个思路启发的作用。
手动🐶头。