服务器 频道

开源 | 携程度假零成本微前端框架-零界

  一、 前言

  1.1 微前端的含义

  在研发一个系统的初期,我们可以把所有代码放到一个项目中。随着企业的发展,业务逻辑越发复杂和专业化,又会细分出不同的研发团队,独立负责其中某一部分。

  每个开发团队有他们各自的迭代节奏,很难在耦合的同一个项目中,满足所有团队的需求。我们很自然地会将整个系统拆解到多个子应用/子项目中,他们可以独立开发、独立部署,但共同协作支撑了系统的整体功能。

  当上述系统拆解过程,发生在后端时,它被称之为——微服务;当它发生在前端时,则被称之为——微前端。

  从某种意义上,微前端是默认值,不需要额外的努力。浏览器一开始就实现了通过超链接的方式,支持多个 HTML 页面之间跳转。

  Tim Berners-Lee, a British scientist, invented the World Wide Web (WWW) in 1989, while working at CERN. The Web was originally conceived and developed to meet the demand for automated information-sharing between scientists in universities and institutes around the world.

  1989年,英国科学家蒂姆-伯纳斯-李在欧洲核子研究中心工作时发明了万维网(WWW)。万维网最初是为了满足世界各地大学和研究所的科学家之间自动分享信息的需求而构思和开发的。

  Web 自它被发明开始,就已经是一种服务于跨团队(不同大学、不同科学组织)之间的沟通与协作的信息技术。

  但是,朴素的页面跳转,往往会在页面过渡阶段产生白屏,在体验上不能满足我们的需求。

  因此,当我们说“微前端”时,我们想要达到的目标是:

  不同的前端团队,可以独立开发和部署他们的应用,满足自己的迭代需求

  多个前端子应用之间的协作与切换,不应该产生不可接受的用户体验下降

  1.2 微前端的类型

  我们可以把微前端按照其拆解的颗粒度,分成:

  页面级微前端(page-level):每个子应用独享一个页面,子应用之间的切换就是页面之间的跳转/切换。

  区域级微前端(section-level):在同一个页面中,存在两类区域:

  a. 共享区域,如顶部菜单栏、侧边栏等,由所有子应用共享。

  b. 切换区域,通常作为主体内容呈现,子应用在该区域做局部切换。

  页面级微前端(page-level)是浏览器的默认功能,但体验不佳;因此,当前大部分微前端框架,致力于区域级微前端(section-level),代表框架有 Qiankun,Single-Spa 等。

  区域级微前端的主要实现思路,可以粗略概括如下:

  代理或劫持 window 环境,让多个子应用及其依赖的前端框架,可以互不干涉地独立运行

  每个子应用注册了“创建”与“销毁”等生命周期,等待主应用根据 url 去驱动和调度它们

  区域级微前端(section-level)可以很好地解决某一类微前端场景(如复杂的后台系统),子应用恰好拥有相同的界面风格,甚至相同的 Layout,如顶部菜单栏、侧边栏等模块,只有内容主体部分有差异。

  然而,在另一些场景中,我们可能仍然需要页面级微前端(page-level)。

  子应用之间拥有不同的 UI 风格,甚至不同的 Layout,它们之间的切换,就是整页的切换,而不是局部的切换。

  我们不希望子应用为了迎合区域级微前端(section-level)的接入要求,而做出巨大的调整,甚至改变开发方式。

  子应用需要同时存在,并且可以在切换过程中,以滑入/滑出的动画方式转场,在回退过程中,可以自动保持滚动条位置等。

  etc。

  今天我们要介绍的——零界微前端,就属于上述页面级微前端(page-level),它克服了子应用切换过程的体验问题。

  二、 零界介绍

  2.1 设计理念

  成本可控。接入成本不应该随着应用的接入数量增加而指数级地上升,接入 2 个应用和接入 100 个应用考虑的问题应该是一致的。

  真正的技术无关。无论应用使用的是什么技术栈、渲染方式是SSR还是CSR、应用类型是SPA还是MPA,都可以无缝接入。

  零耦合。微应用和主应用之间、微应用和微应用之间,完全没有依赖关系。应用的接入和退出不会对应用本身和已经接入的应用带来任何副作用。

  2.2 基本工作原理

  零界作为页面级微前端(page-level)解决方案,在架构上和区域级微前端(section-level)大体一致,但在实现方式上有所不同。

  零界采用经典的基座应用 + 配置的方式来管理子应用。

  在零界中,基座又叫做shell 。shell 只做两件事:存放微应用和调度微应用。

  所有微应用都加载在iframe中,零界通过 shell 管理多个iframe的加载和切换。

  然而,iframe 会带来路由不同步的问题。零界通过 history api 如 pushState 和 replaceState,将当前激活的页面的地址,同步到浏览器地址栏里的 location 中,保持了URL 一致。

  与市面上微前端框架最大的不同是,在零界中没有生命周期、Event Bus等复杂的概念,而是监听微应用的跳转行为,通过将跳转记录存储在浏览器中, 把所有的微应用串联起来。每一次微应用的跳转,新的页面会以 iframe 的形式加载至零界微前端,并且不会立即释放之前微应用的内存,可以快速回退。为了避免过多的 iframe 导致页面卡顿,零界限制了 iframe的最大数量。

  特点:

  无需改造原有代码。技术栈无关,无需担心前端开发的难度。

  几乎零接入成本。每个页面只需引入一个 script 文件,即可加入零界微前端机制。

  无刷新切换页面。提供无刷新页面切换的 SPA 体验,给用户一致性的体验。

  安全可靠。所有页面可随时退出零界微前端机制,回归原始状态。

  状态同步。刷新页面不会丢失路由状态,页面回退更快展示,并保留前一页的滚动条以及页面状态。

  隔离。完全隔离了每个页面的css和js,避免了各个应用之间的变量污染。

  2.3 为什么是 iframe

  构建区域级微前端(section-level)时,由于 iframe 使用简单、自带进程级别隔离等特性,许多开发者都曾考虑使用iframe 构建微前端,但最终都不约而同地放弃了这个方案。

  让我们结合下图,再回顾下利用 iframe 构建区域级微前端(section-level)可能会带来的具体问题。  

区域级微前端页面示意图

  (1)DOM 割裂严重。蒙层只能覆盖其中一个微应用(一块蓝色区域),无法遮住整个应用(整个粉色区域);

  (2)通信困难。不同的微应用同时存在于一个页面,微应用之间需要额外的通讯,而 iframe 只能通过 postmessage 传递序列化的消息,无法满足需求;

  (3)加载慢。一个页面中通常存在多个微应用,微应用会频繁挂载、卸载,iframe 每一次加载都是一次上下文的重新构建;

  (4)路由状态丢失。刷新页面后 iframe 会回到首次加载的状态;

  可以看出,这些痛点是由 iframe 自带的特性导致的,不只是针对区域级微前端(section-level),而是使用 iframe 时要考虑的通用性问题。

  现在,我们再站在页面级微前端(page-level)的角度,逐一思考上面的问题:  

零界微前端页面示意图

  (1)DOM 割裂严重;无需解决✅ 如上图所示,所有微应用的展示都是全屏的,不存在蒙层无法全局展开的问题。

  (2)通信困难;无需解决✅ 各个微应用之间原本就属于完全不同的应用,所以并不用侧重于应用之间的通信。

  (3)加载慢;无需解决✅ 在页面级微前端(page-level)中,每次进入页面只会加载一个微应用(iframe)。

  (4)路由状态丢失;问题同样存在于页面级微前端

  也就是说,我们只需要解决浏览器历史记录同步的问题,就可以最大化利用 iframe 的特性,这就是零界选择 iframe 管理微应用的原因。

  三、 如何使用

  3.1 基本使用

  如上图所示,假设我们现在需要做到上面展示的home Page,page A,page B 和 page C 这4个页面无刷新切换的效果,应该如何实现呢?

  如果他们是同一个应用的不同组件,则可以通过 React 或 Vue 的 TransiitonGroup 等组件快速实现。

  但是,如果他们是 4 个朴素的HTML页面/应用,可能很难通过传统前端框架实现,甚至,大多数区域级微前端(section-level)也无法完成。

  而在零界中,每个微应用都是全屏的,分别存放在 iframe 里,可以通过操作 iframe 的方式来操作微应用,就像把样式叠加在普通的 DOM 元素上一样。

  零界针对 H5 页面模拟了 Native App 中 WebView 切换的机制,也就是上图的切换效果,接入零界即可开箱即用。

  让我们来看下如何搭建零界微前端。

  第 一 步,创建零界shell。

  假设 4 个页面的地址分别为:

  localhost:3000/demo/index.html

  localhost:3000/demo/pageA.html

  localhost:3000/demo/pageB.html

  localhost:3000/demo/pageC.html  

  如上图所示,无需通过 npm/yarn 安装,也无需调用任何函数,只需要对一个普通的HTML页面做两个改动就可以完成 shell 的搭建:

  (1)设置接入零界的微应用的匹配路径。

  (2)引入零界shell脚本,引入后就可获得零界的能力。

  第二步,接入零界。

  在 4 个应用的 HTML中,分别在 head 标签里写入下面的代码  

  我们在接入零界的微应用上,也只做了两个改动:

  (1)配置开启/关闭零界。

  (2)引入零界 page 脚本。

  以上就是构建零界微前端的所需的所有代码。

  通过这样接入有以下 3 个好处:

  (1)没有学习成本,直接引入

  (2)不影响应用本身的 SEO

  (3)在零界中以子应用运行和应用独立运行没有区别,他们的路由路径一致

  另外,我们可以发现,当微应用不能匹配 shell 中配置的路径,或者微应用关闭了零界时,都无法接入零界。

  所以,当一个应用接入零界后导致无法正常访问时,可以通过配置化的方式远程关闭零界,这个页面就会退化为普通页面,而不必等待 shell 去改变配置。并且,这样既不影响零界中已有的微应用跳转,也不影响零界中的微应用跳转至这个页面。

  3.2 零界进阶

  上文展示了朴素页面的切换,体验了零界在 H5 页面的滑入滑出的效果。然而朴素页面并不能满足我们实际的需求。

  想象一下这样一个场景:有多个 CSR 应用,他们共享同一个 Sidebar,但拥有不同的 Content,直接展示它们都会有一段白屏,我们希望在切换时,消除白屏,直接看到更完整内容的页面。

  这是一个常见的 B 端项目优化需求,区域级微前端(section-level)和页面级微前端(page-level)都可以提供解决方案。

  在现代web开发模式中,通常将页面中的内容按功能、区域划分为不同的组件,以提高代码复用性、扩展性。因此 Sidebar 和 Content 可以视为两个不同的组件。

  区域级微前端(section-level)和页面级微前端(page-level)对应用中的组件有不同的处理方式,产生了不同的优化策略:

  区域级微前端(section-level)以组件(区域)为单位,拆分原应用,并重构组件。之后,会从组件的角度,考虑如何在基座应用中主动挂载、卸载,达到想要的效果。

  页面级微前端(page-level)以页面为单位,在不改动原有应用组件的情况下,聚合所有应用。所以聚合之后,会从应用的角度,考虑如何被动式地对内部组件进行优化。

  通过区域级微前端解决,大概分为 4 步:

  (1)将每个应用中的 Sidebar 和 Content 拆分出来。

  (2)把每个 Content 作为一个微应用单独部署,并配置基础信息、添加生命周期。

  (3)将 Sidebar 直接放入基座应用中,或者,作为一个微应用单独部署。

  (4)创建基座应用,注册所有的微应用。

  在切换应用时,只需卸载前一个应用的 Content,加载下一个应用的 Content 既可,共享的 Sidebar 部分并没有变动,完全模拟了在一个应用中的切换体验。

  但是,受限于样式隔离、运行性能、子应用保活等因素,需要在市面上现有的区域级微前端(section-level)解决方案中权衡,很难找到完全满足诉求的方案。并且无论是哪种改造方案,都需要较高改造的成本,这个成本还会随着应用的数量指数级上升。

  让我们来看下页面级微前端会如何解决。

  零界提供了另一种思路,不侵入式地改变原有应用的前提下,优化应用之间的交互。

  改造分为 2 步:

  (1)创建零界 shell,配置接入微应用的路径

  (2)在所有接入的应用中,引入零界 page 脚本

  至此,和之前展示的朴素页面切换效果一致,但是页面的跳转还是产生割裂感。  

  为了提升用户体验,在零界微前端切换页面时,顶部会展现 Progressbar,表示页面切换的进度。

  并且,受 Puppeteer 和 Playwright 这两个e2e测试工具的启发,零界模拟了里面的 waitFor 功能,如上图所示,表示等待 Sidebar 组件展示到页面后,再进行页面切换。这样当多个应用在拥有相同 Sidebar 的页面之间切换时,Sidebar 的部分在视觉上是固定的,只有 Content 发生变化,通过这种方式在多页应用中获得沉浸式的体验。

  不仅如此,还可以通过 timeout 设置最长等待时间,一旦超过等待时间,页面则会强制切换。

  这种优化方式带来以下几个好处:

  应用的 Content 和 Sidebar 的交互,并不需要额外的机制,因为它们本来就是同一个应用的不同组件。

  应用不必须携带相同的 Sidebar,随着业务的发展需要可以更灵活地决定自己的 UI,零界不会是应用扩展的瓶颈。

  让我们来对比下优化前后的效果,为了能更直观地感受其中的差异,我们把网速调整为高速3G。

  未经过任何优化,每个页面都是不同的应用(网速:高速3G)

  经过零界优化后(网速:高速3G)

  可以非常明显地看出,经过零界优化后,多页应用的跳转更为流畅,并且支持快速回退页面。

  细心的读者可能发现,这两个动图的 URL 不一致。这里仅展示零界带来的优化效果,通过本地 Node 代理服务器完成零界跳转,所以和应用原有 URL 不同。在开发企业级项目时,通常不存在这个问题,可以通过SLB等方式快速解决。

  另外,值得一提的是,零界文档也是基于零界微前端构建的,可以直接体验零界在 MPA 中切换的效果,有兴趣的话可以查看零界文档。

  总结

  至此,我们介绍了零界的基本原理和使用方式,其适用场景可以概括如下:

  资源整合。组合多个已有的中大型应用,无需重构。

  MPA优化。将 MPA 的跳转提升至 SPA 的体验。

  H5端体验优化。低成本通过框架的能力达到 WebView 的切换效果。

  虽然零界是页面级微前端(page-level)解决方案,但这并不表示它和区域级微前端(section-level)是冲突的。如果你想快速体验微前端,或者你的项目由于现有微前端框架可能带过高的成本而迟迟没有落地,那么可以先尝试使用零界,在零界中可以随时退出,不会带来任何副作用。若尝试后不能满足需求,再考虑接入颗粒度更细致的区域级微前端(section-level)。

  如果你对这个项目感兴趣,欢迎一起来为零界做贡献。

2
相关文章