qiankun微前端父子应用路由管理(vue-router)
现状
玩qiankun有一段时间了,微前端项目陆续落地上线。调试时发现了一个bug:页面跳转时,浏览器的history无法准确记录,表现为点击浏览器的前进和后退按钮时出现404。
最开始是简单在基座的router.beforeEach里加了一行代码,:
history.replaceState({ back: from.path }, to.name, '')
这个在我之前的文章里有讲解,文章传送门。
实际项目上,如果发现如果只是基座去控制路由做跳转,浏览器的前进回退是没有问题的;但是子应用如果存在新增、编辑、详情等页面,需要子应用自己去控制做跳转,然后再使用浏览器的前进回退就报错了。
解析之后发现,父子应用存在router对象不一致,无法做监听。顺着思路往下,使用同一个router对象,即子应用不再使用自己的路由,页面跳转交由父应用去实现。
实现
新建一个qiankunRouter.js文件,重构路由跳转方法,再加入匹配子页面activeRule的处理。在这里其实存在一个值得商榷的问题,到底是在子应用自己去加还是父应用去处理。父应用处理,子应用改造会更小;子应用处理,灵活性更大。
import router from '@/router'
export class QiankunRouter {
constructor(base = '') {
this.router = router
this.base = base
}
back() {
this.router.back()
}
forward() {
this.router.forward()
}
go(num) {
this.router.go(num)
}
// 父应用处理activeRule
push(params) {
if (typeof params === 'string') {
this.router.push(this.base + params)
} else {
const { path } = params
this.router.push({ ...params, path: this.base + path })
}
}
repalce(params) {
if (typeof params === 'string') {
this.router.repalce(this.base + params)
} else {
const { path } = params
this.router.repalce({ ...params, path: this.base + path })
}
}
}
这里需要注意,后续可能无法使用router.push({name: xxx})的方式去跳转了,不过动态路由的情况下,这个问题可以忽略...的吧?
父应用中的配置文件中给子应用传递路由:
import { QiankunRouter } from './qiankunRouter'
export const subApps = [
{
name: 'microVue', // 子应用名称
entry: 'http://locahost:9527/', // 子应用入口
container: '#subApp', // 挂载子应用的dom
activeRule: '/micro-vue', // 路由匹配规则
props: {
router: new QiankunRouter('/micro-vue'),
token: '',
userInfo: {},
buttons: []
}
}
]
子应用做接收,先创建一个qiankunRouter.js,然后在main.js去对接。
// qiankunRouter.js
import microRouter from '@/router'
class Router {
constructor() {
// 非qiankun模式下,直接走子应用自身的路由
this.router = microRouter
// qiankun模式下,如果是子应用处理activeRule可以使用preRouter
this.preRouter = ''
}
setRouter(router) {
// qiankun模式下,将父应用路由传递过来并接收
this.router = router
}
// 这里也可以做其他处理,比如处理activeRule
setPreRouter(preString) {
this.preRouter = preString
}
// 子应用处理activeRule
push(params, preRouter = '') {
if (typeof params === 'string') {
this.router.push((preRouter || this.preRouter) + params)
} else {
const { path } = params
this.router.push({ ...params, path: (preRouter || this.preRouter) + path })
}
}
repalce(params, preRouter = '') {
if (typeof params === 'string') {
this.router.repalce((preRouter || this.preRouter) + params)
} else {
const { path } = params
this.router.repalce({ ...params, path: (preRouter || this.preRouter) + path })
}
}
}
const qiankunRouter = new Router()
export default qiankunRouter
// main.js
import { createApp } from 'vue'
import router from './router'
import actions from '@/utils/qiankunActions'
import qiankunRouter from '@/utils/qiankunRouter'
import App from './App.vue'
import { renderWithQiankun, qiankunWindow } from 'vite-plugin-qiankun/dist/helper'
let app
const render = container => {
app = createApp(App)
app.use(router).mount(container ? container.querySelector('#app') : '#app')
}
const initQianKun = () => {
renderWithQiankun({
mount(props) {
const { container, router } = props
render(container)
qiankunRouter.setRouter(router) // 传入父应用路由
qiankunRouter.setPreRouter('/micro-vue') // 全局处理activeRule
// actions注入
actions.setActions(props)
// 监听全局状态变化
const userStore = useUserStore()
props.onGlobalStateChange((state, prev) => {
userStore.userInfo = { ...state.userInfo, token: state.token }
userStore.buttons = state.buttons
})
},
bootstrap() {},
unmount() {
app.unmount()
}
})
}
qiankunWindow.__POWERED_BY_QIANKUN__ ? initQianKun() : render()
这样设置之后,子应用单独运行时会使用自身router;作为微前端嵌入到基座中时,会继承父应用的router。最后使用方法如下:
import qiankunRouter from '@/utils/qiankunRouter'
const ceshi = () => {
qiankunRouter.router.push('/detail')
// qiankunRouter.router.push({ path: '/detail', query: { id: 123 } })
}
// or 需要跨子应用跳转时
const ceshi2 = () => {
qiankunRouter.router.push('/detail', '/micro-react')
// qiankunRouter.router.push({ path: '/detail', query: { id: 123 } }, '/micro-react')
}
// 回退或前进时
const goback = () => {
qiankunRouter.router.back()
// qiankunRouter.router.forward()
// qiankunRouter.router.go(-1)
}
总体来说,作为微前端的子应用接入基座时,子应用的路由就只需要起到创建路由的作用,所有路由的跳转全交给基座去完成。这样就能很大程度上避免路由之间版本冲突问题和数据统一处理问题。
2024年12月19日 发现一个bug,复现步骤:
-
- 主应用跳转几个新的子应用路由
-
- 使用浏览器功能回退
-
- 使用浏览器功能前进
-
- 再跳转其他路由,会直接出现报错,报错提示如下:
[Vue Router warn]: Error with push/replace State SecurityError: Failed to execute 'replaceState' on 'History': A history state object with URL 'http://localhost:9527undefined/' cannot be created in a document with origin 'http://localhost:9527' and URL 'http://localhost:9527/micro-vue/dashboard'
vue-router.js?v=856f8e7c:395 Uncaught (in promise) SyntaxError: Failed to execute 'replace' on 'Location': 'http://localhost:9527undefined' is not a valid URL.
通过查询qiankun和vue-router的issue,发现这个问题主要是vue-router4x这个版本中关于history的current赋值错误导致的。想了解细节的可以看一下vue-router的issue#1219。
开始顺着官网加了router.listen = false去掉了监听,虽然意外解决了路由不再跳转两次的问题。但是带来了新的bug,虽然不报错,回退之后点击其他链接也没了反应。这个问题在issue上依然没有人解答。
那就回到最开始的问题,既然是current无法正确赋值(undefined),给个正确的值。
第一次处理是在router.beforeEach里直接写代码:
history.replaceState({ back: from.path, current: to.path }, to.name, '')
结果是第一次点击浏览器的回退需要两次,且会存在丢失历史记录的情况。分析:加上history.state.current之后,确实是正常了,但是history.state.current的值没给对。
于是想到在router.afterEach里配置history.state.current。
router.afterEach((to, from) => {
history.replaceState({ ...history.state, current: to.path }, to.name, '')
})
注意: back还是文章开头的写法,直接在router.beforeEach里赋值。router.afterEach把back的值保留下来,再加上current的值。
最后结果,是目前没有发现类似问题。