无界微前端实践
背景
团队主技术栈为 React,但存在部分特殊的老项目基于 Vue2.x 相关技术栈开发,存在以下痛点:
- 单文件组件的维护性差,存在不少上千行的文件
- 大量 mixin 代码维护性极差,难以排查问题,令人无所适从
- Vue2 对 TypeScript 的兼容差,即使改造了一些模块后,开发体验仍然很差
- 技术栈不统一造成心智负担重
- ElementUI 年久失修,功能缺失且不再维护,很多 Antd 能够满足的需求到 Element 却需要自己实现
虽然存在这么多的问题,但平时的需求开发已经消耗掉了大半的精力,整个项目层面的技术重构专项是绝不会被排进迭代的,所以我们的目标就变成了:在平时的开发过程识别需求规模,当某一模块调整范围较大时对其进行重构,以达到渐进式升级的目的。这样一来会存在很长一段时间的老 Vue 和新 React 应用并行的阶段,这是无可厚非的。
第一阶段我们新建一个 React 项目,将一个较大模块下的所有页面从 Vue 迁移到了 React,在顶部菜单栏切换时在新老站点间进行跳转,但也导致了以下问题:
- 由于跳转前后是两个应用,每次对于路由/菜单/权限等公共逻辑的修改需要在两个项目进行同步,较难维护
- 站点间的跳转通过直接修改 window.location.href 实现,每次跳转过后头部及侧边栏菜单都会重新加载,无法实现平滑过渡
- 由于前一条导致重构动力不足,进度迟缓
重构的目标是什么?提升开发体验 + 用户体验!但到现在为止,我们好像有点违背了初衷?
于是,微前端的接入需求就应运而生了。但正常的需求都做不完呢,我们需要一个低成本接入的方式。我们调研了市面上几乎所有的微前端框架:
- Single-Spa
- Qiankun
- MicroApp
- Wujie
- IceStark
- EMP
- Garfish
Wujie 以超低成本接入,子应用几乎无需改造的优势脱颖而出。
Wujie 是什么
Wujie,中文名无界。它是一个基于 WebComponent 容器 + iframe 沙箱的微前端方案,能够完善的解决适配成本、样式隔离、运行性能、页面白屏、子应用通信、子应用保活、多应用激活、vite 框架支持、应用共享等问题,并且改造成本超低。
为什么选它
最重要的原因:接入低成本,主应用嵌入子应用组件,子应用监听路由跳转即可。
改造步骤
主应用改造
安装 wujie 基于 vue 的框架封装:
bash
pnpm add wujie-vue2
入口文件改造:
js
import WujieVue from 'wujie-vue2'
const { bus } = WujieVue
Vue.use(WujieVue)
/**
* 初始化vue应用
*/
const initInstance = () =>
new Vue({
router,
store,
render: h => h(App),
}).$mount('#app')
if (window.__POWERED_BY_WUJIE__) {
let instance
window.__WUJIE_MOUNT = () => {
instance = initInstance()
}
window.__WUJIE_UNMOUNT = () => {
instance.unmount()
}
/*
由于vite是异步加载,而无界可能采用fiber执行机制
所以mount的调用时机无法确认,框架调用时可能vite
还没有加载回来,这里采用主动调用防止用没有mount
无界mount函数内置标记,不用担心重复mount
*/
window.__WUJIE.mount()
} else {
// 正常vue应用的初始化流程
initInstance()
}
在需要渲染子应用的地方嵌入 wujie 组件:
html
<wujie-vue
width="100%"
:height="'100%'"
name="wujie-react"
:url="childUrl"
:sync="true"
:alive="true"
></wujie-vue>
组件属性说明:
- name 子应用的唯一标识
- url 子应用url
- sync 开启路由同步
- alive 子应用保活
主应用通知子应用路由改变:
js
import WujieVue from 'wujie-vue2'
const { bus } = WujieVue
router.beforeEach((to, from, next) => {
// 通过wujie的事件中心通知子应用
bus.$emit('ROUTE_CHANGE', {
path: to.path,
type: 'navigate',
})
// 自身的路由切换
next()
})
主应用监听子应用内部发起的跳转:
js
import WujieVue from 'wujie-vue2'
const { bus } = WujieVue
bus.$on('CHILD_JUMP', pathname => {
router.push({ path: pathname })
})
子应用改造
子应用监听主应用路由变化事件进行跳转:
ts
import { useNavigate } from 'react-router-dom'
const navigate = useNavigate()
useEffect(() => {
// 子应用监听到主应用发送的路由变化事件后,内部跳转到对应的页面,实现路由同步
window.$wujie?.bus.$on('ROUTE_CHANGE', function (params: { path: string }) {
const { path } = params
navigate(path)
})
}, [])
当子应用内部需要跳转到其他页面时,需要通知主应用:
ts
// 子应用内部跳转其他页面时,需要通知主应用进行跳转,而不是自己跳
// 这样做的原因是需要通知主应用,如果非子应用页面,由主应用渲染,如果是子应用页面,则再通知回子应用进行跳转
export const _jump = (path: string) => {
window.$wujie?.bus.$emit('CHILD_JUMP', path)
}
这样一来,所有的跳转都由主应用进行控制,从而形成路由闭环。