Zoey
Published on

微前端中的隔离错误上报

居然有快半年没更新了... 最近对微前端深入了解(加班)比较多,所以想陆续输出一些围绕微前端的小想法💡。

微前端

微前端是什么就不赘述了,不了解的小伙伴可以看下 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
  },
})

那么问题来了,有没有觉得心智负担陡增?

  1. 子应用编译时、宿主运行时都需要对 meta 进行操作,一旦有变动这在跨部门协作中是比较蛋疼的。
  2. 当一个生产者导出若干个微模块,而这些微模块有的希望是独立的监控,有些是希望聚合查看、同时支持筛选的,那么对于微前端的监控配置将非常复杂。
  3. 有些公司自己的监控轮子,可能根本不支持类似 sentry 的微前端 auto routing 能力。好了,那新的造轮子大业又要开始了。

所以对于一个懂事的微前端框架,是不是可以自己解决这套逻辑?

错误上报的隔离分发思路

从上面 sentry 的例子不难看出,整体的思想是,为每个子应用增加一个标识,这个标识将会在宿主运行时被消费。通过分辨标识的来源,宿主会将错误分发到不同的监控实例(或 id 等任意监控控制面可切换的维度)中去。所以如果我们想自己在微前端框架中实现,那么有两个绕不开的关键点:

  1. 子应用的标识
  2. 错误分发

通常来说,第一点“子应用的标识”对于微前端属于“天然存在的”。在微前端框架设计中,一个很突出的点是沙盒。可以是通过 Proxy, 也可以是任何别的技术实现的沙盒。沙盒就意味着隔离、互不干扰,那么对于每个子应用一定有标识存在,不管是通过运行时还是编译时注入。那么对于错误隔离来说,也是类似的。那么就只需要解决第二个问题 —— 错误分发。

错误分发

错误监控的原理是:对 window 的 errorunhandledrejection 进行监听、处理和上报。所以微前端运行时框架要做的,就是最先注册监听事件,并设置在捕获阶段进行处理。之所以强调捕获, 是为了更早地捕获事件,为父级元素赋予处理事件的能力,特别是在微前端错误监控场景下,便于在宿主侧进行更细粒度的控制。

摸清思路后,接下来是一段伪代码。需要注意一点:在运行框架自身的错误分发逻辑后,要阻止错误事件 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 })

上面的代码只是比较粗略的框架,需要大家细心填补,微前端框架百花齐放,这段也只起一个思路启发的作用。

手动🐶头。