可能是多端适配性最好的微前端框架

612 阅读10分钟

引子

对微前端方向有了解的同学可能都看过 qiankun 的文档,深入的同学估计也去翻过其代码和 single-spa 的源码。一年前我也是受微前端思想的影响,当时刚好面临的业务是变化频繁,需要支持线上功能的热插拔和独立部署。充分调研了微前端和 qiankun 之后,发现很适合当前业务,才开始开始实践微前端的。由于 single-spa 太过于简陋,就采用 qiankun@1.x 作为微前端驱动,今天想把这段时间以来遇到的问题和解决之道跟大家分享一下。

可能面临的业务场景

假设你现在维护或新开发这样一个项目,业务变化频繁,页面功能有重复,但风格是一致的,暂时只有 pc 端。你拿到设计稿后发现,可以把业务分拆成多套 layout,由基座(主应用)提供。功能独立的模块集成在子应用,每个子应用有特殊的 routerBase。

咔咔咔,两三下你做完了所有任务,子应用也能在线配置,支持热插拔和独立部署。爽歪歪~

几天后突然有了以下几个新业务场景,你陷入了沉思。。。

场景一:随着业务的变化,需要支持移动端。

你新建了个子应用,routerBase 设置为 h5。当判断是移动端时自动跳转 /h5/xxx。如果 pc 端访问 /h5/xxx 时,自动重定向到 /xxx,能勉强应付该场景,不过因为基座太重导致移动端渲染很慢,在移动端使用“前进/后退”按钮也会因为重定向太多导致混乱,有时候页面渲染成 pc 页面或无法渲染。

场景二:页面是响应式的,移动端点击登录后,也能共享登录态。

响应式页面是活动页,有活动报名,就要求登录态共享。当用户在移动端点击登录时,理应跳转到移动端的登录页面,登录完重定向到活动页面 需要是登录成功的状态。如果是每个子应用都有 routerBase,来回跳转好几次,而且还要想办法共享登录。

场景三:老板要求 pc 端和移动端 URL 一致。。。

routerBase 天然断绝了这种可能性,根本无法做到!

场景四:老板要求一个四年前的项目聚合到门户站点中。。。

该项目的很多依赖特别是脚手架都已经严重过时,锁定一级依赖的版本但锁不了二级以上的版本,导致构建发布失败。根本无法往项目代码中加入 bootstrap/mount/unmount...!

qiankun 的“kun”(困)境

qiankun 的定位可能是中后台项目的聚合。这类项目彼此间很少依赖,登录态的控制可以在基座中完成,属于一个基座下的多个串联。但面临上述场景时,就显得力不从心。

另外上述场景也暴露了 qiankun 的诸多问题:

  • 基座一般由真实的业务应用承载,提供登录等功能,也提供多套 layout;
  • 基座很容易耦合很多 pc 端的 UI 库和功能代码;
  • 基座很容易沦为通用数据和逻辑的聚集地,虽然这些不是基座应用的职责;
  • 严重依赖 routerBase,框架判断便利但牺牲了灵活性;
  • 多实例共存支持不足,目前仅支持 microApp 形式,且这种应用不建议有路由系统;
  • 严重依赖生命周期 bootstrap/mount/unmount;

破局之道

@icatjs/micro 是我这四个月来实现并在实际项目中实践的新一代微前端框架,不但能轻松应付上述“困境”,还具备其他良好的特性。目前仅有两个方法 registerSubapps 和 start ,其他需要注意的就是子应用的配置了。TA 是如何应对上述困境的呢?听我一一分享下:

端应用(或称 layout 应用)

大家看到这个新名词,不要以为对子应用做了强制的分类。只要某个子应用 a 提供了 layout 或一些路径被用作了 layout,又被其他子应用 b 依赖了,那么该应用 a 就可以看做是“端应用”。a 是不是端应用,自己根本不知道,它会按子应用正常的加载。而 b 会在 a 提供了挂载点后再渲染到页面上。看个例子吧~

这是端应用的配置

{
    "name": "a",
    "entry": "aaa",
    "history": "browser",
    "props": {},
    "rules": [
      {
        "rule": "/",
        "container": "#mountNode",
        "endType": "pc"
      }
    ]
  }

解析下:a 作为 pc 端的应用,路径规则配置的是通配符 / ,这样所有的路由都会被这个应用匹配到。当然会通过 endType 优先判断是哪个端,然后再去处理应用的“依赖链”。目前由于场景比较简单,每个路由下只支持一条依赖链。理论上讲可支持多条依赖链,也就是 多链多实例同时并存。

这是子应用的配置

{
    "name": "b",
    "entry": "bbb",
    "history": "browser",
    "props": {},
    "rootVars": {
      "externals": {
        "@react": "React",
        "@react-dom": "ReactDOM",
        "userInfo": "userInfo"
      }
    },
    "rules": [
      {
        "rule": "/b/activities/1520",
        "layout": "a > /layout/headless",
        "endType": "none"
      },
      {
        "rule": "/b/tec-support",
        "container": "#mountNode",
        "endType": "none"
      },
      {
        "rule": "/development",
        "layout": "a > /layout/basic",
        "endType": "pc"
      }
    ]
  }

也解析下:可以看 rules 中配置了三个典型的规则,第一条依赖了端应用 a 的 /layout/headless,第二条不依赖任何端应用,第三条依赖了 a 的 /layout/basic。

不配置 container 的路径,如果是子应用默认是 #subappMountNodeWrapper ,如果是端应用不依赖任何 layout 则默认是 #appMountNodeAndDoNotCover 。这样如果页面中有这些节点出现时,子应用就会渲染到该节点上。

大家也可以看到,一个子应用的激活与否只和路径规则有关,这些规则遵守 single-spa 的 activeWhen 规则,可以是字符串、函数或数组。而每条路径规则都有自己的 container,也有属于自己的端 endType 和 layout。这样一个子应用既可以作为依赖链上的一个端应用,又可以作为独立的子应用。按路径规则灵活组合,适应多种场景需要。

注意!端应用也可以依赖其他端应用,只需要在其路径规则上配置需要依赖的路径即可。

entry

可以是一个 html 文件路径,也可以是一个数组,包含多个 js、css 和 html。初始化挂载点时,以最后一个 html 的内容填充进去。

全局路由系统

内置处理了 react/vue 多个子应用同时被激活,多个路由系统冲突的问题。无需再像使用 qiankun 那样,还要把子应用的路由配置到主应用中,以防冲突。全局路由会拦截路由变化,把真实路径和 layout 路径分发到各个子应用,使其能正常渲染。 并且,所有子应用都可以有自己独立的 404 页面。可以把子应用完全看成是独立的应用,无需顾虑路由的默认匹配,无需担心路由不匹配时直接进入应用的 404 页面。

沙箱

内置了 iframe 沙箱,子应用的代码运行是在 iframe 中。这样代码的状态都会保存在 iframe 中,不用麻烦提供快照,天然具备快照能力。也内置了垃圾回收队列,当一个子应用 5 分钟内不被激活,会被释放掉 iframe。 注意!这些 iframe 都是空白 iframe,耗费的渲染资源很少。但框架在 iframe 中提供了虚拟 BOM,使这些字应用运行完的结果能够正确渲染到 iframe 外的真实渲染层中。 注意2!子应用的所有对 dom 的操作都被限制在挂载点中,换言之,document.body 指向的是挂载点而不是渲染层的真实 body。这种严密的控制也体现在 getElementById 等方法中。 image.png

当然还有其他一些特性如第三方库共享全局数据共享组件&方法&状态流共享缓存机制等,初次介绍就先不深入了。后续会筹备官网,全面介绍这些特性的。

到此,我们梳理一下困境的解决之道:

  • 场景一,需要支持移动端。由于去掉了 routerBase 的依赖,又支持端应用,天然解决了基座重造成的渲染问题,反复重定向造成的跳转问题。
  • 场景二,页面是响应式的需要共享登录态。由于支持 layout 依赖链,某个端应用可以集中处理登录逻辑,其他端应用依赖其提供的功能即可。逻辑可以通过流共享模式或者全局数据共享,分发到多个子应用。响应式的页面属于依赖链的某个节点,自然能及时获得登录态。
  • 场景三,URL 一致性。由于去掉了 routerBase 的依赖,框架会在初始化时判断所处的终端,进而激活不同端应用。而端应用的路径规则都可以配置成通配符 / ,这样很容易保证 URL 一致。
  • 场景四,四年前的老项目集成。框架支持无生命周期的项目,可以无缝渲染到门户站点中来。这是我测试的截图,大家有兴趣可以看下:

这是集成的 2018 年的 seeconf 官网: thead&seeconf.gif

这是集成的口碑官网: thead&koubei.gif

是否解决了 qiankun 的困境呢?

由于通用逻辑和 layout 被分摊到一层层的“端应用”中,基座只需要做好对框架的引用和对规则的获取与解析,就能驱动起整体的站点运行起来。子应用的路由也不用顾虑冲突,而考虑在主应用中 copy 一份。 基座可以做到很薄,每个端只需加载其需要的 UI 库或组件。比如 pc 端使用了 antd,移动端可以使用 antd-mobile,框架不会把 antd 加载到移动端去。

还有哪些不足?

  • 不建议基座具有路由系统,目前还没去实验基座路由和子应用路由冲突的场景。
  • 多路由系统冲突方案,暂时没有支持 angular,对 vue 的支持可能存在一点儿 bug。
  • 多条依赖链场景暂未支持,目前只处理了第一条依赖链。
  • 子应用中一些 js 灵活写法,如 eval 中的全局变量 var 去定义,包括本身的 var 定义的全局变量,还无法正确挂载到虚拟 window 上。可能存在某些项目加载报错的情况。
  • 梳理了 21 种路由冲突的场景,可能存在例外的情况。

浏览器兼容

主流浏览器 chrome, safari, edge, firefox 等都兼容,国内小众浏览器 360、qq 浏览器等也兼容。优先使用 Proxy,不支持则退化为 Object.defineProperty。

谁在使用

目前平头哥 IoT 团队在做芯片开放社区(OCC),这个框架的诞生也是为了满足其不断变化的业务需求。大家可以看下 pc 端和移动端的 URL 是相同的,但激活的子应用是不同的。

也希望看到本文的同学,在未来某个时刻使用 @icatjs/micro 框架,做出精彩纷呈的作品来!

应一些同学的反馈,随手搞了个 demo,请大家初步了解下该框架的使用。