大家好,我是佑子.最近在工作中,碰到了关于微前端中如何实现主应用和子应用的问题.也查阅了相关资料.但是网络上其实五花八门的答案都有,感觉看了后不是特别清楚.所以特此来写一篇文章.一是分享给大家,让大家避免和我一样.少走弯路.二是记录下来,自己以后如果忘记了方便查阅.
关于微前端这个概念,可能有些小伙伴会比较陌生.这里先给不懂的小伙伴简单介绍一下微前端
什么是微前端
把前端做好很难,让多个团队同时开发大型前端应用,就更难了。目前有一种趋势是将前端应用拆分成更小、更易于管理的小应用。这就是所谓的微前端.简单来说,就是一个应用里面,依赖很多的子应用,而子应用又可以单独启动,形成一个应用,更好的方便管理代码.
Techniques, strategies and recipes for building a modern web app with multiple teams that can ship features independently. — Micro Frontends
微前端是一种多个团队通过独立发布功能的方式来共同构建现代化 web 应用的技术手段及方法策略。
微前端架构具备以下几个核心价值:
-
技术栈无关
主框架不限制接入应用的技术栈,微应用具备完全自主权 -
独立开发、独立部署
微应用仓库独立,前后端可独立开发,部署完成后主框架自动完成同步更新 -
增量升级
在面对各种复杂场景时,我们通常很难对一个已经存在的系统做全量的技术栈升级或重构,而微前端是一种非常好的实施渐进式重构的手段和策略
-
独立运行时
每个微应用之间状态隔离,运行时状态不共享
微前端架构旨在解决单体应用在一个相对长的时间跨度下,由于参与的人员、团队的增多、变迁,从一个普通应用演变成一个巨石应用(Frontend Monolith)后,随之而来的应用不可维护的问题。这类问题在企业级 Web 应用中尤其常见。
目前,市面上有好多方法实现微前端.在我现在做的项目中,则是引用的qiankun这一方案.
qiankun(乾坤)
qiankun 是一个基于 single-spa 的微前端实现库,旨在帮助大家能更简单、无痛的构建一个生产可用微前端架构系统。
Why Not iframe
为什么不用 iframe,这几乎是所有微前端方案第一个会被 challenge 的问题。但是大部分微前端方案又不约而同放弃了 iframe 方案,自然是有原因的,并不是为了 “炫技” 或者刻意追求 “特立独行”。
如果不考虑体验问题,iframe 几乎是最完美的微前端解决方案了。
iframe 最大的特性就是提供了浏览器原生的硬隔离方案,不论是样式隔离、js 隔离这类问题统统都能被完美解决。但他的最大问题也在于他的隔离性无法被突破,导致应用间上下文无法被共享,随之带来的开发体验、产品体验的问题。
- url 不同步。浏览器刷新 iframe url 状态丢失、后退前进按钮无法使用。
- UI 不同步,DOM 结构不共享。想象一下屏幕右下角 1/4 的 iframe 里来一个带遮罩层的弹框,同时我们要求这个弹框要浏览器居中显示,还要浏览器 resize 时自动居中..
- 全局上下文完全隔离,内存变量不共享。iframe 内外系统的通信、数据同步等需求,主应用的 cookie 要透传到根域名都不同的子应用中实现免登效果。
- 慢。每次子应用进入都是一次浏览器上下文重建、资源重新加载的过程。
其中有的问题比较好解决(问题1),有的问题我们可以睁一只眼闭一只眼(问题4),但有的问题我们则很难解决(问题3)甚至无法解决(问题2),而这些无法解决的问题恰恰又会给产品带来非常严重的体验问题, 最终导致我们舍弃了 iframe 方案。
qiankun中实现主应用子应用通信
先来举一个需要实现应用间通信的场景,如下
![image.png]
红色的框是主应用,绿色是子应用.当我想点击版本,进行路由切换时,同时收起侧边栏.控制侧边栏显示隐藏的代码在主应用里,点击版本进行路由切换的代码在子应用里.这里就涉及到了主应用和子应用间的通信.
主应用子应用间的通信有好多方式,这里我分两种来说,一是市场主流框架.例如react,vue.二是蚂蚁内部框架 bigfish实现主应用子应用通信
react,vue实现应用通信(一)
主应用与子应用的通讯只是用了APIinitGlobalState
initGlobalState返回一个MicroAppStateActions对象,它有三个属性:
- onGlobalStateChange:
(callback: OnGlobalStateChangeCallback, fireImmediately?: boolean) => void
, 在当前应用监听全局状态,有变更触发 callback,fireImmediately = true 立即触发 callback - setGlobalState:
(state: Record<string, any>) => boolean
, 按一级属性设置全局状态,微应用中只能修改已存在的一级属性 - offGlobalStateChange:
() => boolean
,移除当前应用的状态监听,微应用 umount 时会默认调用
父应用如何做
import { initGlobalState } from 'qiankun';
const state = {
baiduinit: window,
abc: 456
}
// 初始化通信池
const actions = initGlobalState(state);
// 监听通讯池的变化
actions.onGlobalStateChange((state, prev) => {
// state: 变更后的状态; prev 变更前的状态
console.log(state, prev);
});
复制代码
复制代码
子应用如何做
1.创建action.js
// /src/qiankun/action.js
function emptyAction() {
// 提示当前使用的是空 Action
console.warn("Current execute action is empty!");
}
class Actions {
// 默认值为空 Action
actions = {
onGlobalStateChange: emptyAction,
setGlobalState: emptyAction,
};
/**
* 设置 actions
*/
setActions(actions) {
this.actions = actions;
}
/**
* 映射
*/
onGlobalStateChange() {
return this.actions.onGlobalStateChange(...arguments);
}
/**
* 映射
*/
setGlobalState() {
return this.actions.setGlobalState(...arguments);
}
}
const actions = new Actions();
export default actions;
复制代码
复制代码
2.在main.js mount方法中接收父应用的传值props
import action from './qiankun/action'
export async function mount(props) {
console.log('[vue] props from main framework', props);
action.setActions(props)
render(props);
}
复制代码
复制代码
3.在需要接收父应用传入的参数的地方引用action.js
import action from '@/qiankun/action'
export default {
name: 'Home',
mounted() {
// 接收state
action.onGlobalStateChange((state) => {
console.log(state)
}, true);
},
methods:{
changeValue(){
// 修改state
action.setGlobalState({abc:789})
}
}
}
复制代码
复制代码
子应用可以修改通讯池,修改完会被主应用监听到。
react,vue实现应用通信(二)
该方法我在bigfish框架中用的,react和vue应该也行 原理只是将方法挂在到window上,多一个全局函数
第二种方法相对简单,直接在主应用的main.js这个文件中,将方法挂在到window上面,这样子应用就可以通过window拿到想要的属性和方法了
首先,我们先将上面那个例子的控制侧边栏的状态,保存到redux中,实现组件通信
![image.png]
然后,在主应用的main.js文件中,将修改侧边栏的方法.挂在到全局函数上
![image.png]
接下来,最简单的一步.子应用哪里需要用到,直接访问window即可
![image.png]
到这里,应用通信就完成了.当然,因为我们这是微前端项目,子应用可以单独启动.所以要做一个判断.避免如果单独启动子应用.没有对应的函数时候报错问题.
bigfish实现应用通信
配合 useModel 使用(推荐)
需确保已安装
@umijs/plugin-model
或@umijs/preset-react
- 主应用使用下面任一方式透传数据:
-
如果你用的 MicroApp 组件模式消费微应用,那么数据传递的方式就跟普通的 react 组件通信是一样的,直接通过 props 传递即可:
function MyPage() { const [name, setName] = useState(null); return ( <MicroApp name={name} onNameChange={(newName) => setName(newName)} /> ); } 复制代码
-
如果你用的 路由绑定式 消费微应用,那么你需要在
src/app.ts
里导出一个useQiankunStateForSlave
函数,函数的返回值将作为 props 传递给微应用,如:// src/app.ts export function useQiankunStateForSlave() { const [masterState, setMasterState] = useState({}); return { masterState, setMasterState, }; } 复制代码
-
微应用中会自动生成一个全局 model,可以在任意组件中获取主应用透传的 props 的值。
import { useModel } from '@alipay/bigfish'; function MyPage() { const masterProps = useModel('@@qiankunStateFromMaster'); return <div>{JSON.stringify(masterProps)}</div>; } 复制代码
或者可以通过高阶组件 connectMaster 来获取主应用透传的 props
import { connectMaster } from '@alipay/bigfish'; function MyPage(props) { return <div>{JSON.stringify(props)}</div>; } export default connectMaster(MyPage); 复制代码
- 和
<MicroApp />
的方式一同使用时,会额外向子应用传递一个 setLoading 的属性,在子应用中合适的时机执行masterProps.setLoading(false)
,可以标记微模块的整体 loading 为完成状态。
基于 props 传递
类似 react 中组件间通信的方案
-
主应用中配置 apps 时以 props 将数据传递下去
// src/app.js export const qiankun = fetch('/config').then((config) => { return { apps: [ { name: 'app1', entry: '//localhost:2222', props: { onClick: (event) => console.log(event), name: 'xx', age: 1, }, }, ], }; }); 复制代码
- 子应用在生命周期钩子中获取 props 消费数据
到这里,qiankun中应用通信已经结束了.如果有什么不懂的话有错误的.欢迎在评论区指出
参考资料: