一、背景
尽管 Web 应用的复杂度和参与人数以爆炸式的增长速度,但却没有一种新的架构模式来解决现有的困境,并同时兼顾 DX(developer experience)和 UX(user experience)。
在前端研发日常工作中,我们可能需要同时面对非常多的研发系统,例如:代码管理、代码构建、域名管理、应用发布、CDN 资源管理、对象存储等。站在整个公司研发的角度考虑,最好的产品形态就是将所有的研发系统都放置同一个产品内,用户是无法感知他在使用不同的产品,对于用户而言就是单个产品不存割裂感,也不需要去学习多个平台,仅仅需要学习和了解「研发中台」即可。
以xx公司为例,由于xx公司内部存在大量业务线,每一条业务线都会诞生大量的中台系统,并且还在指数增长,以xx公司电商业务举例,对于电商运营的日常工作来说,其实与研发日常工作一样,围绕在:商品、商家、品牌、风控、营销等工作上,那么对于电商运营来说怎么样才最高效的电商运营系统呢,由于整个系统涉及范围较广,在实际的研发过程中必然会以功能或业务需求垂直的切分成更小的子系统,切分成各种小系统后尽管由于分治的设计理念提升了开发者体验,但是一定程度上降低了用户体验。那能否以一种新的架构模式,既保开发者体验,又能提升用户体验呢。
1.1 传统Web应用的利与弊
传统Web应用在开发大规模应用时,会涉及多研发团队协作的问题。以上面案例中的「电商运营平台」为例,对于电商运营而言商品、商家、品牌等来说,它们都是电商运营平台能力的一部分,若以传统的前端研发模式进行开发,那么此时有两种项目设计策略。
- 将平台内多个系统放置同一个代码仓库维护 ,采用 SPA(Single-page Application) 单页应用模式。
- 将系统分为多个仓库维护,在首页聚合所有平台的入口,采用 MPA(Multi-page Application)多页应用模式。
采用方案一的劣势非常明显,在日常开发中研发:代码构建半小时以上、发布需求时被需求阻塞、无法局部灰度局部升级、项目遇到问题时回滚影响其他业务、无法快速引进新的技术体系提高生产力,项目的迭代和维护对于研发同学而言无疑是噩梦。
尽管降低了开发体验,如果对项目整体的代码拆分,懒加载控制得当,其实对于使用平台的用户而言体验却是提升的,这一切都归因于 SPA 应用带来的优势,SPA 应用跳转页面时无需刷新整个页面,路由变化时仅更新局部,不用让用户产生在 MPA 应用切换时整个页面刷新带来的抖动感而降低体验,并且由于页面不刷新的特性可以极大程度的复用页面间的资源,降低切换页面时带来的性能损耗,用户也不会感知他在使用不同平台。
采用方案二在一定程度上提升了开发体验,但却降低了用户体验,研发在日常开发工作中需要使用大量的平台,但是却需要跳转到不同的平台上进行日常的研发工作,整体使用体验较差。体验较差的原因在于将由于通过项目维度拆分了整体「研发中台」这样的一个产品,使各个产品之间是独立的孤岛,系统间相互跳转都是传统意义上的 MPA,跳转需要重新加载整个页面的资源,除了性能是远不如 SPA 应用的并且应用间是没法直接通信,这就进一步增强了用户在使用产品时的割裂感。
二、微前端架构简介
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化Web应用的技术手段及方法策略。微前端借鉴了微服务的架构理念,将一个庞大的前端应用才分为多个独立灵活的小型应用,每个应用都可以独立开发,独立运行,独立部署,再将这些小型应用联合成一个完整的应用。微前端既可以将多个项目融合为一,可以减少项目之间的耦合,提升项目扩展性,相比一整块的前端仓库,微前端架构下的前端仓库倾向于更小更灵活。
2.1 微前端应用场景
如果出现如下的场景,那么可以考虑使用微前端架构:
- 大型企业应用程序:大型企业应用程序通常需要多个团队协同开发和维护,使用微前端可以将应用程序拆分成多个小型应用程序,每个小型应用程序由不同的团队负责开发和维护,以提高开发效率和代码质量;
- 多渠道应用程序:多渠道应用程序通常需要根据不同的设备和渠道定制不同的功能和界面,使用微前端可以将应用程序拆分成多个小型应用程序,每个小型应用程序专注于不同的渠道和设备,以提高用户体验和应用程序的可扩展性;
- 基于模块的应用程序:基于模块的应用程序通常需要将应用程序拆分成多个小型模块,每个小型模块专注于不同的功能和业务逻辑,使用微前端可以将应用程序拆分成多个小型应用程序,每个小型应用程序由不同的模块组成,以提高代码可维护性和可扩展性;
- 多语言应用程序:多语言应用程序通常需要根据不同的语言定制不同的界面和内容,使用微前端可以将应用程序拆分成多个小型应用程序,每个小型应用程序专注于不同的语言和文化,以提高用户体验和应用程序的国际化能力;
- 前端应用程序复用:前端应用程序复用通常需要将应用程序拆分成多个小型应用程序,每个小型应用程序可以被其他应用程序复用,以提高代码复用性和可维护性。
2.2 微前端架构优点
微前端架构具有如下一些优点:
- 模块化开发:微前端将前端应用程序拆分成多个小型应用程序,每个小型应用程序都是一个独立的模块,可以独立开发和测试,以提高开发效率和代码质量;
- 技术栈无关性:每个小型应用程序可以使用不同的技术栈和框架,不受限于单一的技术栈,以提高灵活性和可扩展性;
- 独立部署和升级:每个小型应用程序可以独立部署和升级,不会影响整个应用程序的运行,以提高部署和升级的效率和可靠性;
- 团队自治性:每个小型应用程序可以由不同的团队开发和维护,不会互相干扰,以提高团队协作效率和开发质量;
- 更好的性能和用户体验:微前端可以将前端应用程序拆分成多个小型应用程序,只加载必要的模块和组件,以提高应用程序的性能和用户体验;
- 可复用性和可测试性:每个小型应用程序都是一个独立的模块,可以被其他应用程序复用,也可以独立测试和调试,以提高代码质量和可维护性。
2.3 微前端架构模式
目前,主流的微前端架构模式有如下一些:
基于路由的微前端:将前端应用程序拆分成多个小型应用程序,并根据路由规则将它们组合成一个完整的应用程序。每个小型应用程序都有自己的路由和页面组件,可以独立开发和部署。这种方式通常使用前端框架的路由机制来实现。
基于组件的微前端:将前端应用程序拆分成多个小型应用程序,并通过组件化的方式将它们组合成一个完整的应用程序。每个小型应用程序都有自己的组件和数据模型,可以独立开发和部署。这种方式通常使用 Web Components、React、Vue 等前端框架的组件化机制来实现。
基于消息的微前端:将前端应用程序拆分成多个小型应用程序,并通过消息机制来进行通信和协作。每个小型应用程序都有自己的状态和数据模型,可以通过事件和消息来进行共享和协作。这种方式通常使用 Redux、MobX 等状态管理工具来实现数据共享。
基于 API 的微前端:将前端应用程序拆分成多个小型应用程序,并通过 API 来进行通信和协作。每个小型应用程序都有自己的 API 接口和数据模型,可以通过 API 来进行共享和协作。这种方式通常使用 RESTful API、GraphQL 等 API 技术来实现数据共享。
同时,实现微前端架构模式,需要考虑以下几方面:
- 应用程序拆分:将前端应用程序拆分成多个独立模块的小型应用程序;
- 路由和组合:将多个小型应用程序通过路由和组合的方式组合成一个完整的应用程序,可以根据业务需求和用户操作动态加载和卸载组件与模块;
- 共享状态:将应用程序的状态和数据通过事件和消息的方式进行共享;
- 构建和部署:每个小型应用程序都需要独立构建和部署,可以使用不同的构建工具和部署方案,以保证独立性;
- 团队协作:每个小型应用程序可以由不同的团队开发和维护,需要建立良好的团队协作机制和沟通渠道,以保证整个应用程序的一致性和稳定性。
三、微前端架构整体方案
如何提供一套解决方案既具备 SPA 的用户体验,又能够具备 MPA 应用带来的灵活性,并且可以实现应用间同灰度,监控也可能细化到子系统呢。目前,在XX公司应用的微前端解决方案可以作为参考,解决方案主要分为三层:部署侧、框架运行时、调试工具,目前采用的是 SPA 的架构,架构示意图如下。
3.1 微服务部署平台
部署平台作为微前端研发流程中重要的一环,主要提供微前端的服务发现、服务注册、子应用版本控制、多个子应用间同灰度、增量升级子应用、下发子应用信息列表,分析子应用依赖信息提取公共基础库降低不同应用的依赖重复加载。
同时,Serverless 平台提供的接口或在渲染服务中下发主应用的 HTML 内容中包含子应用列表信息,列表中包括了子应用的详细信息例如:应用 id、激活路径、依赖信息、入口资源等信息,并通过对于子应用的公共依赖进行分析,下发子应用的公共依赖,在运行时获取到子应用的信息后注册给框架,然后在主应用上控制子应用进行渲染和销毁。
3.2 iFrame容器方案
谈到微前端,一个绕不开的话题就是:为什么不使用 iFrame 作为承载微前端子应用的容器。从浏览器原生的方案来说,iFrame 作为一个非常古老的微前端技术方案却一直很管用。不过,采用 iFrame方案,有几个重要的前提:
- 网站不需要 SEO 支持
- 拥有相应的应用管理机制
总的来说,使用iFrame微前端方案,有如下一些劣势: - 使用 Iframe 会大幅增加内存和计算资源,因为 iframe 内所承载的页面需要一个全新并且完整的文档环境
- Iframe 与上层应用并非同一个文档上下文导致
- 事件冒泡不穿透到主文档树上,焦点在子应用时,事件无法传递上一个文档流
- 跳转路径无法与上层文档同步,刷新丢失路由状态
- Iframe 内元素会被限制在文档树中,视窗宽高限制问题
- Iframe 登录态无法共享,子应用需要重新登录
- Iframe 在禁用三方 cookie 时,iframe 平台服务不可用
- Iframe 应用加载失败,内容发生错误主应用无法感知
- 难以计算出 iframe 作为页面一部分时的性能情况
- 无法预加载缓存 iframe 内容
- 无法共享基础库进一步减少包体积
- 事件通信繁琐且限制多
3.3 基于 SPA 的微前端架构
尽管难以将 Iframe 作为微前端应用的加载器,但是却可以参考其设计思想,一个传统的 Iframe 加载文档的能力可以分为四层:文档的加载能力、HTML 的渲染、执行 JavaScript、隔离样式和 JavaScript 运行环境。那么微前端库的基础能力也可以参考其设计思想。
从设计层面采取的是基座+子应用分治的概念,部署平台负责进行服务发现和服务注册,将注册的应用列表信息下发至基座,通过基座来动态控制子系统的渲染和销毁,并提供集中式的模式来完成应用间的通信和应用的公共依赖管理,因此一个基于SPA的微前端架构至少需要考虑以下几个方面的能力。
加载器(Loader)
- 负责注册平台侧提供的应用列表
- 负责加载和解析子应用入口资源,如HTML 入口类型,拆解 HTML Dom、Script、Style;JS 入口类型,提供基础 Dom 容器。
- 预加载能力
- 解析子应用导出内容
沙箱隔离(Sandbox)
- 提供代码执行能力,收集执行代码时存在的副作用
- 提供销毁收集副作用的能力
- 支持沙箱多实例,收集不同实例的副作用
路由托管(Router)
- 解决不同应用间的路由不同步问题
- 提供路由劫持能力,在主应用上管控子应用路由
- 提供路由驱动能力来拼装完整的平台的能力
子应用通信(Store)
-
建立通信桥梁
-
提供共享机制
3.4 沙箱设计
其实,在过去的 Web 应用中是很少提及到沙箱这一概念的,因为组件的开发一般都会由研发通过研发规范来尽可能的去避免组件对当前应用环境造成副作用,诸如:组件渲染后添加了定时器、全局变量、滚动事件、全局样式并且在组件销毁后会及时的清除子应用对当前环境产生的副作用。 与组件完全不同的是微前端是由多个独立运行的应用组成的架构风格,这些系统可能分别来自不同的技术体系。项目的开发、测试从空间和时间上都是分离的,由于没有 Iframe 一样原生能力的隔离,所以很难不发生冲突,而这些冲突可能会导致应用发生异常、报错、甚至不可用等状态。
以 Webpack4 JsonpFunction 为例 在 Webpack5 中提供了一个重要的功能就是 Module Federation,随着 Webpack 5 推出 Module Federation ,与 Webpack 4 发生变化的一个重要配置就是 JsonpFunction 属性变为了 chunkLoadingGlobal,并且由原来的默认值 webpackJsonp 变成了默认使用 output.library 名称或者上下文中的 package.json 的 包名称(package name)作为唯一值(webpack.js.org/issues/3940)。
为什么会发生这个转变呢,如果了解过 Webpack 构建产物的一定会知道 Webpack 通过全局变量存储了分 chunk 后的产物,用于解决分包 chunk 的加载问题。由于 Webpack 5 引入 Module Federation 页面中可能会同时存在两个以上的 Webpack 构建产物,如果还是通过同一个变量存储要加载的 chunk ,必然会造成产物之间的互相影响。
通过 Webpack 4 到 Webpack 5 支持 Module Federation 之后可以发现,在一个基础库尚未考虑默认兼容多实例的场景下,贸然将其作为多实例使用很可能会造成应用无法按照预期运行,更为严重的是你以为其正常运行了其实应用已经发生了严重的内存泄漏或不可预知的情况,倘若将 Webpack 构建产物的应用多次动态的在页面中运行,将会发现已经造成严重的内存泄漏,因为 Webpack 会增量的向全局存储 chunk 的变量上挂载模块以及依赖信息,简单来说就是每次执行 Webpack 构建的子应用代码都会向 webpackJsonp 数组 push 大量的数据,最终造成内存泄漏,直至页面崩溃。
那如何设计沙箱呢?沙箱的主要能力也在于能够捕获动态创建的副作用,对应用的副作用进行隔离和清除。因为,为了能够有效的捕获到动态创建的副作用、收集、并隔离,这里提供两种思路:一种是快照模式,另外一种是 VM 模式。
快照沙箱
顾名思义,在应用运行前通过快照的模式来保存当前执行环境,在应用销毁后恢复回应用之前的执行环境,用于实现应用间副作用的隔离和清除。类似于 “SL 大法”,通过 save 存储环境,通过 load 加载环境的模式。
实现思路如下:
-
针对每一种副作用提供一个 Patch 类,这个类需要提供 save 和 load 两个方法。
-
Save 对应着该副作用的环境快照存储,Load 对应着销毁该副作用的销毁恢复环境。
-
并且针对每一种 Patch 还可以存储其在运行期间发生的变化,在优化场景时并不用所有代码,仅恢复执行环境即可。
VM沙箱
通过快照沙箱的最简化的核心实现后可以发现,它的设计理念依赖于整个代码的执行属于线性的过程,即:存储执行环境=>执行具备副作用的代码=>恢复执行环境,但在实际的场景中对于应用的划分并非以页面为维度划分,同一个页面可能存在多个应用,所以它的执行顺序并非线性,可能同时存在多个快照沙箱的实例环境,也就是快照沙箱多实例。以下面例子为例:
// 收集代码运行期间的副作用
let sandboxA = new SnapshotSandbox);
let sandboxB = new SnapshotSandbox);
// 若同时存在两个应用,沙箱则无法有效的判断,其副作用的来源
sandboxA.save();
// 当前宿主环境执行代码
execScript(MainAppCode) ;
// 应用A 执行代码
execScript (AppACode ) ;
// 沙箱 B 存储执行环境
sandboxB.save();
// 应用B 执行代码
execScript(AppBCode) ;
sandboxA. load (); sandboxB. load();
通过上面的代码可以发现,在同时运行多个快照沙箱实例时,在代码执行顺序非线性的场景下,并不能有效的收集和处理应用的副作用,也基于此快照沙箱无法使用在非线性呢多实例的场景中,因此也进一步推出了 VM(virtual machine) 沙箱。
维基百科关于 VM 的解释:在计算机科学中的体系结构里,是指一种特殊的软件,可以在计算机平台和终端用户之间创建一种环境,而终端用户则是基于虚拟机这个软件所创建的环境来操作其它软件。虚拟机(VM)是计算机系统的仿真器,通过软件模拟具有完整硬件系统功能的、运行在一个完全隔离环境中的完整计算机系统,能提供物理计算机的功能。
在 Node 中也提供了 VM 模块,不过不过不同于传统的 VM,它并不具备虚拟机那么强的隔离性,并没有从模拟完整的硬件系统,仅仅将指定代码放置在特定的上下文中编译并执行代码,所以它无法用于不可信来源的代码。
参考 Node 中 VM 模块的设计,以及 JavaScript 词法作用域 的特性,可以设计出 VM 沙箱,不过与传统的 VM 差异也同样存在,它并能执行不可信的代码,因为它的隔离能力仅限于将其运行在一个指定的上下文环境中。
四、主流方案
4.1 Single-SPA
Single-SPA是一款流行的开源微前端框架,可以与目前流行的React、Angular、Vue等前端框架无缝集成。它使用 JavaScript 应用程序作为独立的模块,可以独立加载和卸载,具有良好的灵活性和可扩展性。Single-SPA通过劫持路由的方式来做子应用之间的切换,接入方式要融合自身的路由。
以下是基于Vue3的构建部署过程:
1,创建微应用
使用 Vue CLI 创建一个新的 Vue 3 微应用项目:
vue create micro-app-vue3
2,配置微应用
在 Vue 3 中,需要使用 single-spa-vue@^2.0.0 版本。
npm install single-spa-vue
在微应用项目的 vue.config.js 文件中,配置 publicPath,确保它是微应用在单独部署时的路径。
module.exports = {
publicPath: '/micro-app-vue3/',
};
3,注册微应用
在微应用的入口文件(通常是 src/main.js)中,使用 single-spa-vue 注册微应用。
import { createApp } from 'vue';
import singleSpaVue from 'single-spa-vue';
import App from './App.vue';
const vueLifecycles = singleSpaVue(createApp, App);
export const bootstrap = vueLifecycles.bootstrap;
export const mount = vueLifecycles.mount;
export const unmount = vueLifecycles.unmount;
4,主应用集成
在主应用中安装 single-spa 和 single-spa-vue,并注册微应用。
npm install single-spa single-spa-vue
接着,在主应用的入口文件中注册微应用。
import { registerApplication, start } from 'single-spa';
import { createApp } from 'vue';
import App from './App.vue';
const app = createApp(App);
app.use(/* 插件 */);
registerApplication(
'micro-app-vue3',
() => import('micro-app-vue3/main.js'), // 微应用的入口文件路径
(location) => location.pathname.startsWith('/micro-app-vue3')
);
start();
5,启动与部署
在主应用中启动应用。部署主应用和微应用,确保微应用可以通过主应用的入口地址被正确加载。
参考文档:zh-hans.single-spa.js.org/docs/gettin…
4.2 Qiankun
Qiankun是阿里开源的一个基于 Single-SPA 的微前端框架,专注于解决多个前端应用程序之间的路由、状态管理和样式隔离等问题。支持 React、Angular、Vue 等主流前端框架,并提供了简单的配置和快速的部署流程。
优点
相较于Single-SPA ,Qiankun有如下一些优势:
- 更轻量:Qiankun 的核心库相对较小,更轻量
- 配置简单:对于单个微应用的接入,配置更简单
- 样式隔离好:Qiankun的Shadow DOM,避免了全局样式冲突
- 自动化注入应用间通信机制:支持 props、Vuex、Redux 等多种数据通信方式,方便应用之间传递数据
- 运行性能好:Qiankun 使用了微任务队列,在不同微应用之间切换更流畅
快速上手
1,主应用
首先,在项目中安装Qiankun插件。
yarn add qiankun
# 或者
npm i qiankun -S
在主应用的入口文件中注册微应用。
import { registerMicroApps, start } from 'qiankun';
registerMicroApps([
{
name: 'react app', // app name registered
entry: '//localhost:7100',
container: '#yourContainer',
activeRule: '/yourActiveRule',
},
{
name: 'vue app',
entry: { scripts: ['//localhost:7100/main.js'] },
container: '#yourContainer2',
activeRule: '/yourActiveRule2',
},
]);
start();
当微应用信息注册完之后,一旦浏览器的 url 发生变化,便会自动触发 qiankun 的匹配逻辑,所有 activeRule 规则匹配上的微应用就会被插入到指定的 container 中,同时依次调用微应用暴露出的生命周期钩子。如果微应用不是直接跟路由关联的时候,你也可以选择手动加载微应用的方式:
import { loadMicroApp } from 'qiankun';
loadMicroApp({
name: 'app',
entry: '//localhost:7100',
container: '#yourContainer',
});
2,微应用
微应用不需要额外安装任何其他依赖即可接入 qiankun 主应用。同时,微应用需要在自己的入口 js (通常就是你配置的 webpack 的 entry js) 导出 bootstrap、mount、unmount 三个生命周期钩子,以供主应用在适当的时机调用。
/**
* bootstrap 只会在微应用初始化的时候调用一次,下次微应用重新进入时会直接调用 mount 钩子,不会再重复触发 bootstrap。
* 通常我们可以在这里做一些全局变量的初始化,比如不会在 unmount 阶段被销毁的应用级别的缓存等。
*/
export async function bootstrap() {
console.log('react app bootstraped');
}
/**
* 应用每次进入都会调用 mount 方法,通常我们在这里触发应用的渲染方法
*/
export async function mount(props) {
ReactDOM.render(<App />, props.container ? props.container.querySelector('#root') : document.getElementById('root'));
}
/**
* 应用每次 切出/卸载 会调用的方法,通常在这里我们会卸载微应用的应用实例
*/
export async function unmount(props) {
ReactDOM.unmountComponentAtNode(
props.container ? props.container.querySelector('#root') : document.getElementById('root'),
);
}
/**
* 可选生命周期钩子,仅使用 loadMicroApp 方式加载微应用时生效
*/
export async function update(props) {
console.log('update props', props);
}
除了代码中暴露出相应的生命周期钩子之外,为了让主应用能正确识别微应用暴露出来的一些信息,微应用的打包工具需要增加如下配置:
//webpack v5
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
chunkLoadingGlobal: `webpackJsonp_${packageName}`,
},
};
//webpack v4
const packageName = require('./package.json').name;
module.exports = {
output: {
library: `${packageName}-[name]`,
libraryTarget: 'umd',
jsonpFunction: `webpackJsonp_${packageName}`,
},
};
参考链接:qiankun.umijs.org/zh/guide
4.3 MicroApp
MicroApp采用了基于 Web Components的技术栈,不依赖特定的前端框架,可以与任意前端技术栈集成。它提供了一套完整的解决方案,包括构建、打包、路由、通信等,适用于构建复杂应用系统的微前端架构,适用于需要将多个独立的前端应用集成在一起的场景。
优势
- 使用简单:我们将所有功能都封装到一个类WebComponent组件中,从而实现在基座应用中嵌入一行代码即可渲染一个微前端应用。
- 功能强大:micro-app提供了js沙箱、样式隔离、元素隔离、路由隔离、预加载、数据通信等一系列完善的功能。
- 兼容所有框架:为了保证各个业务之间独立开发、独立部署的能力,micro-app做了诸多兼容,在任何前端框架中都可以正常运行。
快速开始
1,主应用
安装依赖:
npm i @micro-zoe/micro-app --save
初始化MicroApp:
// index.js
import microApp from '@micro-zoe/micro-app'
microApp.start()
嵌入子应用
<template>
<div>
<h1>子应用👇</h1>
<!-- name:应用名称, url:应用地址 -->
<micro-app name='my-app' url='http://localhost:3000/'></micro-app>
</div>
</template>
2,子应用
在webpack-dev-server的headers中设置跨域支持:
devServer: {
headers: {
'Access-Control-Allow-Origin': '*',
}
},
4.4 无界
优势
无界是一款基于 【Web Components + iframe】的微前端架构,具备成本低、速度快、原生隔离、功能强等优点。wujie继承了iframe的优点,补足 iframe 的缺点,让 iframe 焕发新生。
Web Components 是一个浏览器原生支持的组件封装技术,可以有效隔离元素之间的样式,iframe 可以给子应用提供一个原生隔离的运行环境,相比自行构造的沙箱 iframe 提供了独立的 window、document、history、location,可以更好的和外部解耦。
无界微前端框架的几点优势:
多应用同时激活在线
-
框架具备同时激活多应用,并保持这些应用路由同步的能力
组件式的使用方式 -
无需注册,更无需路由适配,在组件内使用,跟随组件装载、卸载
应用级别的 keep-alive -
纯净无污染
- 无界利用
iframe和webcomponent来搭建天然的js隔离沙箱和css隔离沙箱 - 利用
iframe的history和主应用的history在同一个top-level browsing context来搭建天然的路由同步机制 - 副作用局限在沙箱内部,子应用切换无需任何清理工作,没有额外的切换成本
- 无界利用
-
性能和体积兼具
- 子应用执行性能和原生一致,子应用实例
instance运行在iframe的window上下文中,避免with(proxyWindow){code}这样指定代码执行上下文导致的性能下降,但是多了实例化iframe的一次性的开销,可以通过 preload 提前实例化 - 体积比较轻量,借助
iframe和webcomponent来实现沙箱,有效的减小了代码量
开箱即用
- 子应用执行性能和原生一致,子应用实例
-
不管是样式的兼容、路由的处理、弹窗的处理、热更新的加载,子应用完成接入即可开箱即用无需额外处理,应用接入成本也极低
快速接入
1,主应用
主应用不限技术栈,只需引入 wujie、配置子应用路由并启动 wujie 即可。wujie 针对 React 和 Vue 框架分别提供了 wujie-react wujie-vue2 wujie-vue3 依赖。这里以 Vue3 为例,首先在项目中安装wujie需要的依赖。
yarn add wujie-vue3
pnpm add wujie-vue3
npm i wujie-vue3
创建一个 Vue 页面(如 src/views/SubApp.vue)用于承载子应用。
<template>
<div>
<WujieVue width="100%" height="100%" name="sub-app" url="http://localhost:8381/" sync />
</div>
</template>
WujieVue有一些参数:
- name: 子应用唯一标识符。
- url: 子应用的路径地址。
- sync: 是否开启路由同步。开启后 wujie 会将子应用的 name 作为一个 url 查询参数,实时同步子应用的路径作为这个查询参数的值,这样分享 URL 或者刷新浏览器子应用路由都不会丢失。
接下来,还需要配置子应用的路由:
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes: [
...,
{
name: 'sub-app',
path: '/sub-app',
component: () => import('../views/SubApp.vue')
},
]
})
export default router
2,子应用
无界对子应用的侵入非常小,在满足跨域条件下子应用可以不用改造。前提是子应用的资源和接口的请求都在主域名发起,所以会有跨域问题,子应用必须做cors 设置。
app.use((req, res, next) => {
// 路径判断等等
res.set({
"Access-Control-Allow-Credentials": true,
"Access-Control-Allow-Origin": req.headers.origin || "*",
"Access-Control-Allow-Headers": "X-Requested-With,Content-Type",
"Access-Control-Allow-Methods": "PUT,POST,GET,DELETE,OPTIONS",
"Content-Type": "application/json; charset=utf-8",
});
// 其他操作
});
在微前端框架中,子应用放置在主应用页面中随着主应用页面的打开和关闭反复的激活和销毁,而在 wujie 中子应用是否保活以及是否进行生命周期的改造会进入完全不同的处理流程。
图片
- 保活模式:子应用的 alive 设置为 true 时进入保活模式,内部的数据和路由的状态不会随着页面切换而丢失。
- 单例模式:子应用的 alive 设置为 false 且进行了生命周期改造时进入单例模式。
- 重建模式:子应用既没有设置为保活模式,也没有进行生命周期的改造则进入了重建模式。每次页面切换不仅会销毁承载子应用 dom 的 Web Component,还会销毁承载子应用 JavaScript 的 iframe,相应的 wujie 实例和子应用实例都会被销毁。
其中,保活模式、重建模式子应用无需做任何改造工作,单例模式需要做生命周期改造。对于生命周期的改造,主要是改造入口函数: - 将子应用路由的创建、实例的创建渲染挂载到window.__WUJIE_MOUNT函数上。
- 将实例的销毁挂载到window.__WUJIE_UNMOUNT上。
- 如果子应用的实例化是在异步函数中进行的,在定义完生命周期函数后,请务必主动调用无界的渲染函数 window.__WUJIE.mount()。
以下是Vite项目生命周期改造的示例:
declare global {
interface Window {
// 是否存在无界
__POWERED_BY_WUJIE__?: boolean;
// 子应用mount函数
__WUJIE_MOUNT: () => void;
// 子应用unmount函数
__WUJIE_UNMOUNT: () => void;
// 子应用无界实例
__WUJIE: { mount: () => void };
}
}
if (window.__POWERED_BY_WUJIE__) {
let instance: any;
window.__WUJIE_MOUNT = () => {
const router = createRouter({ history: createWebHistory(), routes });
instance = createApp(App)
instance.use(router);
instance.mount("#app");
};
window.__WUJIE_UNMOUNT = () => {
instance.unmount();
};
/*
由于vite是异步加载,而无界可能采用fiber执行机制
所以mount的调用时机无法确认,框架调用时可能vite
还没有加载回来,这里采用主动调用防止用没有mount
无界mount函数内置标记,不用担心重复mount
*/
window.__WUJIE.mount()
} else {
createApp(App).use(createRouter({ history: createWebHistory(), routes })).mount("#app");
}
如果子应用的实例化是在异步函数中进行的(如 Vite),在定义完生命周期函数后,需要主动调用 wujie 的渲染函数 window.__WUJIE.mount()。
通信方式
wujie 提供三种通信方式,分别是props 通信、eventBus 通信和window 通信。
props 通信
主应用可以通过 props 注入数据和方法:
<WujieVue name="xxx" url="xxx" :props="{ data: xxx, methods: xxx }"></WujieVue>
子应用可以通过 $wujie 对象来获取数据:
const props = window.$wujie?.props;
eventBus 通信
wujie 提供一套去中心化的通信方案,主应用和子应用、子应用和子应用都可以通过这种方式方便的进行通信。
// 主应用
import WujieVue from "wujie-vue";
const { bus } = WujieVue;
// 子应用
window.$wujie?.bus
// 监听事件
bus.$on("事件名字", (arg1, arg2, ...) => {});
// 发送事件
bus.$emit("事件名字", arg1, arg2, ...);
// 取消事件监听
bus.$off("事件名字", (arg1, arg2, ...) => {});
window 通信
由于子应用运行的 iframe 的 src 和主应用是同域的,所以相互可以直接通信。
//主应用调用子应用的全局数据
window.document.querySelector("iframe[name=子应用id]").contentWindow.xxx;
//子应用调用主应用的全局数据
window.parent.xxx;