微前端是什么
微前端的灵感来源于服务端微服务的理念,即将 Web 应用由单一的单体应用转变为多个小型前端应用聚合为一的应用。各个前端应用还可以独立运行、独立开发、独立部署。
目的或者是使用场景
- 后台比较分散,体验差别大,因为要频繁跳转导致操作效率低,希望能统一收口的一个系统内
- 单页面应用非常庞大,多人协作成本高,开发/构建时间长,依赖升级回归成本高
- 系统有二方/三方接入的需求
微前端优缺点
优点:
- 应用自治:独立开发,独立部署,独立运行。
- 单一职责:每个前端应用可以专注于完成特定的功能。
- 技术栈无关:微前端架构支持多种技术栈,如Angular、React和Vue等可以并存,这对于拥有不同技术偏好的团队来说是一个优势。
缺点:
- 拆分的粒度越小,便意味着架构变得复杂、维护成本变高,测试和部署变复杂。
- 用户体验不连贯:不同的团队可能使用不同的技术栈和框架。
- 子应用间的资源共享能力较差(某些静态资源需要单独拷贝一份)
本质上来说,我们是在用整个系统的复杂度代价换取单个前端的简洁度。
现有微前端解决方案:
| 方案 | 描述 | 技术栈 | 优点 | 缺点 |
|---|---|---|---|---|
| iframe | 通过 iframe的方式将这些应用嵌入到父应用系统中 | 无限制 | 1. 技术栈无关,子应用独立构建部署 2. 实现简单,子应用之间自带沙箱,天然隔离,互不影响 | 多域名鉴权问题,体验差、路由无法记忆、页面适配困难、依赖无法复用等都具有局限性,资源开销巨大,通信困难 |
| Nginx 路由转发 | 通过Nginx配置实现不同路径映射到不同应用 | 无限制 | 简单、快速、易配置 | 在切换应用时触发页面刷新,通信不易 |
| Web Components | 是一个浏览器原生支持的组件封装技术 | 无限制 | 自定义元素 shadow DOM 支持隔离 | shadow 兼容性支持度不够好 |
| webpack5 模块联邦 | webpack5 模块联邦 去中心模式、脱离基座模式。每个应用是单独部署在各自的服务器,每个应用都可以引用其他应用,也能被其他应用所引用 | 统一技术栈 | 学习成本低,各个应用的资源都可以相互共享 | 需要升级Webpack5技术栈必须保持一致改造旧项目难度大 |
| single-spa | 通过路由路径来在dom上加载不同的子应用 | 无限制 | 轻量级 | 社区规模可能较小,主要关注微应用的加载和管理,对于通信、状态管理等高级功能,可能需要结合其他库或自行实现 |
微前端框架选型
常见实现框架:
(1)qiankun乾坤: 蚂蚁金服开发维护,基于 Single-SPA, 提供了更加开箱即用的 API ( single-spa + sandbox + import-html-entry ) 做到了,技术栈无关、并且接入简单
(2)MicroApp:京东出品,一款基于WebComponent的思想,轻量、高效、功能强大的微前端框架
(3)single-spa:微服务化的 JavaScript 前端解决方案 (本身没有处理样式隔离, js 执行隔离) 实现了路由劫持和应用加载
(4)无界:腾讯开发维护,实现预加载,应用保活(类似keep-alive),配置成本低;但是2022年推出,时间太短,缺乏社区打磨
除此持外还有piral,mosaic等框架,综合对比考虑后,选择使用更成熟的蚂蚁qiankun接入项目,原因如下:
(注:截止2023,qiankun官方不支持vite,目前使用qiankun集成vite构建的vue项目需要依赖插件)
微前端:乾坤 VS 无界
qiankun实现原理
1、监视路由
- 通过监听hashChange和popState这两个原生事件来检测路由变化
2、匹配子应用
- 重写路由window.popState, replaceState,在保留原有功能的基础上,增加子应用entry映射相关逻辑
- 针对变化的路由,匹配子应用
3、加载子应用
- import-html-entry 解析入口文件中的html 和 script
- 动态创建script去执行jsCode
- 通过umd模块获取子应用,调用子应用mount方法,render子应用
4、渲染子应用
- 把js和html,渲染到提前预留的#app容器中
无界(iframe+ web component)
- 官方demo:无界react-demo展示
使用iframe有三个难以解决的问题,
- 路由状态丢失,刷新一下,iframe的url状态就丢失了
- dom割裂严重,弹窗只能在iframe内部展示,无法覆盖全局
- 通信非常困难,只能通过postmessage传递序列化的消息
无界微前端框架通过继承iframe的优点,解决iframe的缺点,打造一个接近完美的iframe方案
在应用A中构造一个shadow和iframe,然后将应用B的html写入shadow中,js运行在iframe中,注意 iframe 的 url,iframe保持和主应用同域但是保留子应用的路径信息,这样子应用的js可以运行在iframe的location和history中保持路由正确。
在iframe中拦截document对象,统一将dom指向shadowRoot,此时比如新建元素、弹窗或者冒泡组件就可以正常约束在shadowRoot内部。
- dom割裂严重的问题,主应用提供一个容器给到shadowRoot插拔,shadowRoot内部的弹窗也就可以覆盖到整个应用A
- 路由状态丢失的问题,浏览器的前进后退可以天然的作用到iframe上,此时监听iframe的路由变化并同步到主应用,如果刷新浏览器,就可以从url读回保存的路由
- 通信非常困难的问题,iframe和主应用是同域的,天然的共享内存通信,而且无界提供了一个去中心化的事件机制
qiankun应用间通信
- 第一种是
qiankun官方提供的通信方式 -Actions通信,适合业务划分清晰,比较简单的微前端应用,一般来说使用第一种方案就可以满足大部分的应用场景需求。 - 第二种是基于
redux实现的通信方式 -Shared通信,适合需要跟踪通信状态,子应用具备独立运行能力,较为复杂的微前端应用。 - 官方提供的 props 主应用: import { registerMicroApps, start, } from "qiankun"; import router from '@/router' const apps = [ { name: "App1MicroApp", entry: '//localhost:9001', container: "#app1", activeRule: "/app1", props: { parentRouter: router } } ];
registerMicroApps(apps); start(); 子应用: let instance = null let router = null
function render (props) { router = new VueRouter({ base: window.POWERED_BY_QIANKUN ? '/app1' : '', mode: 'history', routes: routes }) instance = new Vue({ router, store, // 挂载在根节点上 data(){ return { parentRouter: props.parentRouter, } }, render: (h) => h(App) }).$mount('#app') }
export async function mount(props) { render(props); }
// 在子应用中使用就可以访问到这个parentRouter了 this.$root.parentRouter
沙箱隔离机制
| 框架 | 特点 |
|---|---|
| 乾坤 | js隔离机制: SnapshotSandbox、LegacySandbox、ProxySandbox css隔离机制:strictStyleIsolation、experimentalStyleIsolation |
| 无界 | js通过iframe来隔离,css通过webcomponent + shadowdom来隔离 |
沙箱隔离类型和原理
SnapshotSandbox 快照沙箱
顾名思义,快照沙箱就是,给你拍一张照片,记录你此时的状态,乾坤的快照沙箱是根据diff 来实现的,主要用于不支持window.Proxy的低版本浏览器,而且只是使用单个的自用
缺点
- 需要遍历window上的所有属性,性能差
- 同时间内只能激活一个微应用
代理沙箱
Qiankun 基于es6 的Proxy 实现了两种应用场景不同的沙箱,一种是legacySandbox(单例),一种是proxySandbox(多例)。因为都是基于Proxy实现的,所以称为代理沙箱。
legacySandbox实现原理
legacySandbox 设置三个参数来记录全局变量,分别记录沙箱新增的全局变量addedPropsMapInSandbox,记录沙箱更新的全局变量modifiedPropsOriginalValueMapInSandbox,用于在任意时刻做snapshot的currentUpdatePropsValueMap。
proxySandbox(多例沙箱)
激活沙箱后,每次对window 取值的时候,先从自己沙箱环境的fakeWindow 里面找,如果不存在,就从
rawWindow(外部的window)里去找;当对沙箱内部window 对象赋值的时候,会直接操作fakeWindow,而不会影响到rawWindow
参考链接
juejin.cn/post/723602… juejin.cn/post/724407… www.cnblogs.com/weichen913/… zhuanlan.zhihu.com/p/578093950