V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
aos
V2EX  ›  前端开发

极简微前端框架-京东 MicroApp 开源了

  •  3
     
  •   aos · 2021-07-28 16:26:14 +08:00 · 2385 次点击
    这是一个创建于 1249 天前的主题,其中的信息可能已经有所发展或是发生改变。

    前言

    MicroApp 是一款基于类 WebComponent 进行渲染的微前端框架,不同于目前流行的开源框架,它从组件化的思维实现微前端,旨在降低上手难度、提升工作效率。它是目前市面上接入微前端成本最低的框架,并且提供了 JS 沙箱、样式隔离、元素隔离、预加载、资源地址补全、插件系统、数据通信等一系列完善的功能。MicroApp 与技术栈无关,也不和业务绑定,可以用于任何前端框架和业务。

    本篇文章中我们会从业务背景、实现思路介绍 MicroApp,也会详细介绍它的使用方式和技术原理。

    背景

    随着这些年互联网的飞速发展,很多企业的 web 应用在持续迭代中功能越来越复杂,参与的人员、团队不断增多,导致项目出现难以维护的问题,这种情况 PC 端尤其常见,许多研发团队也在找寻一种高效管理复杂应用的方案,于是微前端被提及的越来越频繁。

    微前端并不是一项新的技术,而是一种架构理念,它将单一的 web 应用拆解成多个可以独立开发、独立运行、独立部署的小型应用,并将它们整合为一个应用。

    在实际业务中,我们也遇到同样的问题,并且在不同的业务场景下尝试了各种解决方案,如 iframe 、npm 包、微前端框架,并对各种方案的优劣进行了对比。

    iframe:在所有微前端方案中,iframe 是最稳定的、上手难度最低的,但它有一些无法解决的问题,例如性能低、通信复杂、双滚动条、弹窗无法全局覆盖,它的成长性不高,只适合简单的页面渲染。

    npm 包:将子应用封装成 npm 包,通过组件的方式引入,在性能和兼容性上是最优的方案,但却有一个致命的问题就是版本更新,每次版本发布需要通知接入方同步更新,管理非常困难。

    微前端框架:流行的微前端框架有 single-spa 和 qiankun,它们将维护成本和功能上达到一种平衡,是目前实现微前端备受推崇的方案。

    由于 iframe 和 npm 包存在问题理论上无法解决,在最初我们采用 qiankun 作为解决方案,qiankun 是在 single-spa 基础上进行了封装,提供了 js 沙箱、样式隔离、预加载等功能,并且与技术栈无关,可以兼容不同的框架。

    业务诉求

    qiankun 虽然优秀,但依然无法满足我们的预期。第一个问题是在我们实际使用场景中,每个接入微前端的项目运行已久,且每个项目由不同的人员和团队负责,如何降低对源代码的侵入性,减少代码修改和沟通成本,这是我们非常关心的点,所以我们需要一种比 qiankun 接入成本更小的方案。第二个问题是在多方应用接入的情况下,沙箱并不能完美规避所有问题,但 qiankun 处理此类不可预料的问题的能力并不是非常高效。在不停的摸索中,我们找到一种极致简洁的实现思路,它像使用组件一样简单,只修改一点点代码就可以接入微前端,并且还提供插件系统,赋予开发者灵活处理问题的能力。

    image

    实现思路

    微前端分为主应用和子应用,主应用也称为基座应用,是其它应用的容器载体,子应用则是被嵌入方。我们分别从主应用和子应用的角度出发,探寻一种更简洁和有效的接入微前端的方式。

    关于 qinkun 和 single-spa 的思考

    在 single-spa 和 qiankun 中都是通过监听 url change 事件,在路由变化时匹配到渲染的子应用并进行渲染。这种基于路由监听渲染是 single-spa 最早实现的,作为出现最早、最有影响力的微前端框架,single-spa 被很多框架和公司借鉴,也导致目前实现的微前端的方式大多是基于路由监听。

    同时 single-spa 要求子应用修改渲染逻辑并暴露出三个方法:bootstrap 、mount 、unmount,分别对应初始化、渲染和卸载,这也导致子应用需要对入口文件进行修改。这个特点也被 qiankun 继承下来,并且需要对 webpack 配置进行一些修改。

    image

    基于路由监听的实现方式和对子应用入口文件以及 webpack 配置的修改是必须的吗?

    其实并不是,微前端的核心在于资源加载与渲染,iframe 的渲染方式就是一个典型,只要能够实现一种元素隔离的功能并且路由符合要求,子应用理论上不需要修改代码就可以嵌入另外一个页面渲染,我们试图从这个角度中找到不一样的实现思路。

    微前端的组件化

    要想简化微前端的实现步骤,必须摒弃旧的实现思路,探索出不同的道路。

    我们借鉴了 WebComponent 的思想,以此为基础推出另一种更加组件化的实现方式:类 WebComponent + HTML Entry 。

    image

    HTML Entry:是指设置 html 作为资源入口,通过加载远程 html,解析其 DOM 结构从而获取 js 、css 等静态资源来实现微前端的渲染,这也是 qiankun 目前采用的渲染方案。

    WebComponent:web 原生组件,它有两个核心组成部分:CustomElement 和 ShadowDom 。CustomElement 用于创建自定义标签,ShadowDom 用于创建阴影 DOM,阴影 DOM 具有天然的样式隔离和元素隔离属性。由于 WebComponent 是原生组件,它可以在任何框架中使用,理论上是实现微前端最优的方案。但 WebComponent 有一个无法解决的问题 - ShadowDom 的兼容性非常不好,一些前端框架在 ShadowDom 环境下无法正常运行,尤其是 react 框架。

    类 WebComponent:就是使用 CustomElement 结合自定义的 ShadowDom 实现 WebComponent 基本一致的功能。

    由于 ShadowDom 存在的问题,我们采用自定义的样式隔离和元素隔离实现 ShadowDom 类似的功能,然后将微前端应用封装在一个 CustomElement 中,从而模拟实现了一个类 WebComponent 组件,它的使用方式和兼容性与 WebComponent 一致,同时也避开了 ShadowDom 的问题。并且由于自定义 ShadowDom 的隔离特性,Micro App 不需要像 single-spa 和 qiankun 一样要求子应用修改渲染逻辑并暴露出方法,也不需要修改 webpack 配置。

    我们通过上述方案封装了一个自定义标签micro-app,它的渲染机制和功能与 WebComponent 类似,开发者可以像使用 web 组件一样接入微前端。它可以兼容任何框架,在使用方式和数据通信上也更加组件化,这显著降低了基座应用的接入成本,并且由于元素隔离的属性,子应用的改动量也大大降低。

    使用方式

    接下来我们将分别介绍主应用和子应用的接入方式。

    以 react 代码举例

    主应用

    每个自定义标签micro-app渲染后就是一个微前端的子应用,它的使用方式类似于 iframe 标签。

    我们需要给标签传递三个基础属性:

    • name:名称
    • url:子应用页面地址
    • baseurl:baseurl 是基座应用分配给子应用的路由前缀

    使用方式如下:

    image

    子应用

    如果子应用只有一个页面,没有路由配置,则不需要做任何修改。

    如果子应用是多页面,只需要修改路由配置,添加路由前缀。

    如下:

    window.__MICRO_APP_BASE_URL__是由基座应用下发的路由前缀,在非微前端环境下,这个值为 undefined

    image

    完成以上配置即可实现微前端的渲染,对源码的改动量很少。当然 MicroApp 还提供了其它一些能力,如插件系统、数据通信,我们接下来做详细介绍。

    核心原理

    MicroApp 的核心功能在 CustomElement 基础上进行构建,CustomElement 用于创建自定义标签,并提供了元素的渲染、卸载、属性修改等钩子函数,我们通过钩子函数获知微应用的渲染时机,并将自定义标签作为容器,微应用的所有元素和样式作用域都无法逃离容器边界,从而形成一个封闭的环境。

    概念图

    image

    渲染流程

    通过自定义元素micro-app的生命周期函数connectedCallback监听元素被渲染,加载子应用的 html 并转换为 DOM 结构,递归查询所有 js 和 css 等静态资源并加载,设置元素隔离,拦截所有动态创建的 script 、link 等标签,提取标签内容。将加载的 js 经过插件系统处理后放入沙箱中运行,对 css 资源进行样式隔离,最后将格式化后的元素放入micro-app中,最终将micro-app元素渲染为一个微前端的子应用。在渲染的过程中,会执行开发者绑定的生命周期函数,用于进一步操作。

    流程图

    image

    元素隔离

    元素隔离源于 ShadowDom 的概念,即 ShadowDom 中的元素可以和外部的元素重复但不会冲突,ShadowDom 只能对自己内部的元素进行操作。

    MicroApp 模拟实现了类似的功能,我们拦截了底层原型链上元素的方法,保证子应用只能对自己内部的元素进行操作,每个子应用都有自己的元素作用域。

    元素隔离可以有效的防止子应用对基座应用和其它子应用元素的误操作,常见的场景是多个应用的根元素都使用相同的 id,元素隔离可以保证子应用的渲染框架能够正确找到自己的根元素。

    概念图

    image

    实际效果

    image

    如上图所示,micro-app元素内部渲染的就是一个子应用,它还有两个自定义元素 micro-app-headmicro-app-body,这两个元素的作用分别对应 html 中的 head 和 body 元素。子应用在原 head 元素中的内容和一些动态创建并插入 head 的 link 、script 元素都会移动到micro-app-head中,在原 body 元素中的内容和一些动态创建并插入 body 的元素都会移动到micro-app-body中。这样可以防止子应用的元素泄漏到全局,在进行元素查询、删除等操作时,只需要在micro-app内部进行处理,是实现元素隔离的重要基础。

    可以将micro-app理解为一个内嵌的 html 页面,它的结构和功能都和 html 页面类似。

    插件系统

    微前端的使用场景非常复杂,即便有沙箱机制也无法避免所有的问题,所以我们提供了一套插件系统用于解决一些无法预知的问题。

    插件可以理解为符合特定规则的对象,对象中提供一个函数用于对资源进行处理,插件通常由开发者自定义。

    插件系统的作用是对传入的静态资源进行初步处理,并依次调用符合条件的插件,将初步处理后的静态资源作为参数传入插件,由插件对资源内容进一步的修改,并将修改后的内容返回。插件系统赋予开发者灵活处理静态资源的能力,对有问题的资源文件进行修改。

    插件系统本身是纯净的,不会对资源内容造成影响,它的作用是统筹各个插件如何执行,当开发者没有设置插件时,则传入和传出的内容是一致的。

    image

    js 沙箱和样式隔离

    js 沙箱通过 Proxy 代理子应用的全局对象,防止应用之间全局变量的冲突,记录或清空子应用的全局副作用函数,也可以向子应用注入全局变量用于定制化处理。

    样式隔离是指对子应用的 link 和 style 元素的 css 内容进行格式化处理,确保子应用的样式只作用域自身,无法影响外部。

    MicroApp 借鉴了 qiankun 的 js 沙箱和样式隔离方案,这也是目前应用广泛且成熟的方案。

    预加载

    MicroApp 提供了预加载子应用的功能,它是基于 requestIdleCallback 实现的,预加载不会对基座应用和其它子应用的渲染速度造成影响,它会在浏览器空闲时间加载应用的静态资源,在应用真正被渲染时直接从缓存中获取资源并渲染。

    image

    资源地址补全

    微前端中经常出现资源丢失的现象,原因是基座应用将子应用的资源加载到自己的页面渲染,如果子应用的静态资源地址是相对地址,浏览器会以基座应用所在域名地址补全静态资源,从而导致资源丢失。

    资源地址补全就是将子应用静态资源的相对地址补全为绝对地址,保证地址指向正确的资源路径,这种操作类似于 webpack 在运行时设置 publicPath 。

    image

    生命周期

    在微应用渲染时,micro-app元素在不同渲染阶段会触发相应的生命周期事件,基座应用可以通过监听事件来进行相应的操作。 image

    生命周期列表:

    • created:当 micro-app 标签被创建后,加载资源之前执行。
    • beforemount:资源加载完成,正式渲染之前执行。
    • mounted:子应用已经渲染完成后执行
    • unmount:子应用卸载时执行。
    • error:当出现破坏性错误,无法继续渲染时执行。

    在卸载时,子应用也会接收到一个卸载的事件,用于执行卸载相关操作。

    数据通信

    数据通信是微前端中非常重要的功能,实现数据通信的技术方案很多,优秀的方案可以提升开发效率,减少试错成本。我们也研究了 qiankun 等微前端框架数据通信的方式,但他们的实现方式并不适合我们,我们尝试直接通过元素属性传递复杂数据的形式实现数据通信。

    对于前端研发人员最熟悉的是组件化的数据交互的方式,而自定义元素 micro-app 作为类 WebComponent,通过组件属性进行数据交互必然是最优的方式。但 MicroApp 在数据通信中遇到的最大的问题是自定义元素无法支持设置对象类型属性,例如<micro-app data={x: 1}></micro-app> 会转换为 <micro-app data='[object Object]'></micro-app>,想要以组件化形式进行数据通信必须让元素支持对象属性。

    为了解决这个问题,我们重写了micro-app元素原型链上属性设置的方法,在micro-app元素设置对象属性时将传递的值保存到数据中心,通过数据中心将值分发给子应用。

    MicroApp 中数据是绑定通信的,即每个micro-app元素只能与自己指向的子应用进行通信,这样每个应用都有着清晰的数据链,可以避免数据的混乱,同时 MicroApp 也支持全局通信,以便跨应用传递数据。

    数据通信概念图

    image

    框架对比

    为了更直观的感受 Micro App 和其它框架的区别,我们使用一张图进行对比。 image 从对比图可以看出,目前开源的微前端框架中有的功能 MicroApp 都有,并提供了一些它们不具备的功能,比如静态资源地址补全,元素隔离,插件系统等。

    业务实践

    MicroApp 已经在公司内部多个项目中使用,表现良好,尤其是将一些老项目改造成微前端,在项目不受影响的情况下,即降低接入成本,又可以保证项目平稳运行,减小耦合。

    为什么开源?

    当初我们团队打算使用微前端时,调研了市面上实现微前端的框架,可供选择的只有 sigle-spa 和 qiankun 。single-spa 太过于基础,对原有项目的改造过多,成本太高。剩下的只有 qiankun,但因为接入很多老项目,在实际使用中出了很多问题,我们不得不对 qiankun 的源码进行大量的魔改。在此过程中,我们对微前端的实现方式产生了一些自己的想法,并将这些想法付诸实践,于是有了 MicroApp 。

    目前像 qiankun 类似提供完善功能的微前端框架太少了,当接入 qiankun 失败时,没有其他方案可供选择,这是我们当初经历过的痛。所以我们选择将 MicroApp 开源,一是因为 MicroApp 有诸多创新点,可以更简单的接入微前端,功能更加丰富,二是可以让大家多一种选择,没有完美的微前端框架,只有选择多了,才知道哪一个更适合自己。

    如果你对这个项目感兴趣,可以通过加入组织或提 pull requests 的方式参与共建,非常欢迎与期待你的加入。

    导航

    GitHub 地址: https://github.com/micro-zoe/micro-app

    官网地址: https://cangdu.org/micro-app

    特别鸣谢:qiankun

    6 条回复    2021-07-29 15:28:24 +08:00
    Yadomin
        1
    Yadomin  
       2021-07-28 16:29:51 +08:00
    qiankun 的图标放成了 umi 的
    oh
        2
    oh  
       2021-07-28 16:38:38 +08:00
    标题写是京东的
    而官网是 cangdu.org/micro-app
    然后打开 cangdu.org 显示的是 cangdu 的个人网站

    这到底是以京东名义的作品,还是某位京东员工的作品?
    Kilerd
        3
    Kilerd  
       2021-07-28 16:40:11 +08:00
    好家伙,react 里面再套一个框架!
    66beta
        4
    66beta  
       2021-07-28 16:41:36 +08:00
    aos
        5
    aos  
    OP
       2021-07-28 16:57:20 +08:00
    @oh 哈哈,公司发版太麻烦,先用自己的,等稳定了切换到内部
    litujin1123
        6
    litujin1123  
       2021-07-29 15:28:24 +08:00
    @aos 这样一看,个人维护可能就不想用了
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2588 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 26ms · UTC 06:09 · PVG 14:09 · LAX 22:09 · JFK 01:09
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.