加载与切换

应用加载与切换

在对于微前端的应用认知上,最初的场景是所谓单实例场景,即控制台上同时运行的只有一个应用,我们会从应用 A 切换到应用 B,从应用 B 切换到应用 C,但我们不会同时把应用 A 和应用 B 都挂载在页面上,这是一种单实例的场景。

应用与控件

实际上我们觉得微前端还是有一种多实例场景。你在一个页面里完全可以挂载多个微应用,你可以同时把应用 A、应用 B、应用 C 和应用 D 合计 4 个应用全部在同一个页面里。这个时候就面临一个问题,应用和组件有什么区别?这也很像之前的几位同学所提到的,weiget 或者微模块,因为在这种情况下子应用往往是没有自己的路由的,与路由无关,它确实在某种程度上很像一个组件,页面的这一部分是一个组件或者说是一个微应用,表现出来的差距并不大。

那怎么样说你是一个组件,又怎么样说你是一个微应用?对我们来说,我们认为能独立开发,独立部署,自己是能够独立完成一些功能的,其实就是一个微应用。它和 weiget 或者说微模块能做到的事情是类似的,两者之间并没有明确的界限,比如说你完全可以把一个组件升格为一个应用,或者把一个应用做成一个组件,这看你的具体场景和选择。

应用路由与 Future State

首先我们来看应用路由的问题。在微前端体系结构下,路由的划分往往是如图这个样子的,这也是一种比较简单的方案。我们主应用加载了应用 A 和应用 B。这个时候对于主应用来讲,我就有两个路由 /a/*/b/* 。在路由 A 下我会加载应用 A,在另一个路由 B 下加载应用 B。

那当我在路由 /b/list 下重刷页面的时候会发生什么过程?实际上我需要先加载我的主应用,我的主应用检测到这是一个 /b/ 打头的路由,于是它知道我应该去加载应用 B,随后去加载了应用 B。到了此时应用 B 接管了路由,它发现后面的路由是 /list ,于是它显示出 /list 的正确页面,总体上就是这么一个过程。

如果你没有做一些处理的话,在最早加载主应用的时候,就会面临 404 和路由报错,因为这个时候应用 B 的路由系统并没有加载进来,你并不知道 /list 需要显示什么。这实际上是懒加载就会面临的问题。早年 Angular 社区把这个问题叫做 Future State。要解决这个问题,比较简单的方式就是我们去劫持一下路由系统,做一些改造,同样的,通过借助像 react-router 之类的路由库,你也可以选用它的动态路由方案,这些都是可以的。

社区比较成熟的方案,就是 Single-SPA;Single-SPA 已经劫持的路由,帮我们做好了加载这件事情,也帮我们做好了路由切换这件事情,在这个方面我们就没有自己造轮子了。

应用接入:协议接入

什么样的一个应用能够成为子应用,能够接入到我们的框架应用里?由于我们需要技术栈无关,所以我们希望接入是一个 协议接入。只要你的应用实现了 bootstrap 、mount 和 unmount 三个生命周期钩子,有这三个函数导出,我们的框架应用就可以知道如何加载这个子应用。

这三个钩子也正好是子应用的生命周期钩子。当子应用第一次挂载的时候,我们会执行 bootstrap 做一些初始化,然后执行 mount 将它挂载。如果你是一个 React 技术栈的子应用,你可能就在 mount 里面写 ReactDOM.render ,把你的 ReactNode 挂载到真实的节点上,把应用渲染出来。当你应用切换走的时候,我们会执行 unmount 把应用卸载掉,当它再次回来的时候(典型场景:你从应用 A 跳到应用 B,过了一会儿又跳回了应用 A),这个时候我们是不需要重新执行一次所有的生命周期钩子的,我们不需要从 bootstrap 开始,我们会直接从 mount 阶段继续,这就也做到了应用的缓存。

App Entry 抉择

在这个方面,qiankun 还面临着另一些选择,其中一个就是 App Entry 选择,也就是如何设计子应用的加载入口,在这个问题上,我们有两个地方需要面临抉择:一个是什么时候去组合,另一个是加载子应用的入口是什么。

组合时机选择

在组合时机的选择上,我们有两个选择:第一个是在构建时,把主子应用打包在一起,着实际上就是一种多包的方案。这个方案它的好处是构建的时候可以做公共依赖的提取,但是它的缺点在于我们把主子应用构建方案和工具都耦合在一起了,这非常不灵活地,这样也没办法做到动态加载。所以在绝大部分情况下,我们都会选择运行时去组合,就是运行的时候才去动态的加载子应用,把它加载、渲染到框架应用里。

说明 优点 缺点
构建时 子应用与主应用一起打包发布 构建时容易做公共依赖提取等优化 主子应用构建方案、工具耦合,必须一起发布
运行时 子应用独立构建,运行时动态加载 主子应用完全解耦,技术栈无关 有运行时损耗

应用入口选择

那我们子应用的入口又如何选择?qiankun 在这里选择的是 HTML,就是以 HTML 作为入口。大家看了刚才的分享,其实已经清楚了:加载子应用用的时候,我们实际上是需要提供一份资源列表,在这份资源列表里,我们可能列出了这个子应用使用了哪些 JS,有哪些 CSS。

但是 qiankun 的第一选择其实是 HTML 入口,就是提供一份 HTML 文件。因为这份 HTML 中其实包含了子应用的所有信息。它包含网页的结构,包含了一些元信息。大家可以看到在这份 HTML 里我们有 CSS、JS 链接、有应用要挂载的根路由 root 的 DOM。这些信息是非常全面的,比单纯你拿 JS 和 CSS 组成一份资源列表作为入口,要清晰和完整得多。同时 HTML Entry 这样的设计,也使得我们在接入一些老旧应用的时候,更加简单。

这里做一个简单的对比,用了 HTML Entry 可以把子应用和主应用更加的解耦了。但是如果你是选择资源列表或者 JS 作为子应用的入口,那么子应用挂载的时候,DOM 节点往往需要和主应用作一个约定,这就产生了一定程度的耦合。

说明 优点 缺点
HTML HTML 作为子应用入口 解耦更彻底,子应用不依赖主应用 DOM;子应用独立开发,能独立部署 多了一次对 HTML 的请求;解析有性能损耗;无法做到构建时优化
JS JS 作为子应用入口 便于做构建时优化 依赖主应用提供挂载节点,打包产物体积膨胀,资源无法并行加载