Qiankun实践——彻底解决无法切换路由的问题
前言
哈喽,大家好,我是海怪。
今天想跟大家分享一个在使用 Qiankun 中遇到的问题,相信不少人也会遇到过,那就是:在主应用切换路由时,无法切换对应子应用的路由。
好吧,这句话听着就不像人话。不过,如果你遇过同样的问题,那么应该能够理解我的意思。不理解的同学也别慌,下面我来慢慢跟大家解释一下这个现象。
这篇文章的代码都放在了 这个仓库 qiankun-bigass-app 的 【routes】 分支上,需要的自行提取即可。废话不多说,我们现在就开始吧。
项目背景
我是怎么发现这个问题的呢?下面来说一下我的项目背景:
当时,我到新部门的时候,这里一共有好几个项目(项目的名字都用了化名,大家看个意思就可以了):
其中,绿色的项目都是一些比较小且专一的管理后台,而红色的项目则是一个功能更广且泛的管理后台。面对这么分散的项目,我们希望通过 Qiankun 的微前端架构能力来管理这些项目。具体来说就是 把红色项目作为主应用基座,绿色的项目作为子应用,把部分子应用的功能慢慢集成到红色的大平台上。
不过在实现的时候,我们并不是直接开 4 个 Tab,然后一个 Tab 对应一个项目来集成。因为这样做的话,用户就要点击很深入才能找到对应的功能,用户更需要的是将所有功能平铺开来,而且希望有取舍地集成:只将部分重要的功能集成到大平台上,剩余的功能由各自项目来承担:
对于页面来说,大概是长这样的:
发现问题
为了实现上面这样的效果,我们希望主应用侧边栏的每一个 Tab 对应一个路由,当点击这个路由的时候,就会加载对应的子应用,以及该子应用对应的那个路由页面。
举个例子:
- 点击 “用户信息”,主应用路由会切换到
/userMicroApp/userinfo
- 然后主应用加载 UserMicroApp 微应用
- 微应用 UserMicroApp 发现切换到了
/userinfo
路由,所以它也会切换到对应的 “用户信息” 页
假如要实现 “历史记录” 和 “房间信息” 的切换,那么,只需要在主应用里多加几个路由就可以了,下面用 “淘宝首页” 和 “关于页” 来做个简化:
<HashRouter>
<div className="app">
<div className="sidebar">
<h1>LOGO</h1>
<ul>
<li>
<Link to='/taobao/home'>淘宝首页</Link>
</li>
<li>
<Link to='/taobao/about'>淘宝关于</Link>
</li>
</ul>
</div>
<Routes>
<Route path="/taobao/home" element={null} />
<Route path="/taobao/about" element={null} />
</Routes>
{/* 微应用挂载的地方 */}
<div id="subapp-viewport" />
</div>
</HashRouter>
主应用的注册逻辑也很简单:
registerMicroApps(
[
{
name: 'taobao',
entry: '//localhost:7101',
container: '#subapp-viewport',
loader: (loading) => setLoading(loading),
activeRule: '/#/taobao',
},
],
);
start();
在微应用中,也要有对应的 /home
以及 /about
的路由配置:
<Router basename={window.__POWERED_BY_QIANKUN__ ? '/taobao' : '/'}>
<nav>
<Link to="/">Home</Link>
<Link to="/about">About</Link>
</nav
<Suspense fallback={null}>
<Switch>
<Route path="/home" exact component={Home} />
<Route path="/about" component={About} />
</Switch>
</Suspense>
</Router>
神奇的地方来了:你会发现点击主应用的 <Link>
并没有任何作用,而点击 Taobao <nav>
里的 <Link>
是可以正常切换。这就是标题里说的 “主应用切换路由时,不更新子应用路由” 的问题。 具体可以看 这个 Issue。
分析原因
为什么主应用切换了路由,子应用的路由不更新呢?面对这种难以排查的问题,我们要把整个链路理清楚:
- 主应用点击
<Link>
- 触发事件
- 主应用捕获到了路由变更事件
- 主应用切换了页面 / 加载子应用
- 子应用也捕获到了路由变更事件
- 子应用切换页面
我们先在主应用里添加一些简单的路由页面,以此检查主应用能否做切换,而显然主应用是能够正常切换页面的:
上面的 “About” 展示的就是主应用的页面。从这个现象我们可以分析出:造成这个问题的主要原因是:子应用没有正常切换页面。
但是,我们在子应用里使用 <nav>
的 <Link>
也是可以切换的,那么说明子应用的路由切换也没问题的。因此,最终我们可以把问题定位到:主应用做路由切换的时候,子应用无法捕获主应用的路由切换事件。
路由实现原理
那这个 Trigger 事件是什么呢?这里就要说说 React Router 或 Vue Router 的实现原理了。
无论是 Vue 还是 React,它们本质上都差不多,一共有两种模式 Hash 以及 History,这里以 Hash 为主。这两种模式分别有两类触发方式:JS 代码触发,比如 history.push
;以及用户在浏览器的一些操作,比如点击前进、后退、输入地址等。
对于前者,Router 会再度封装 history.push
变成自己 Router 的 routerHistory.push
,每次 Push 的时候,Router 会做状态变更、寻找页面组件、更新页面等等操作。而对于后者,Router 会监听两种模式下的不同事件,以此又来做状态变更、寻找页面组件、更新页面等等操作。整体如下图所示:
解决问题
现在回到我们这个例子。
对于 Hash 模式,Vue Router 以及 React Router 都对 push
函数进行了优化,比如 Vue 会判断当前是否支持 history.pushState
,如果有则直接用,否则才用 window.location.hash = xxx
:
export const supportsPushState =
inBrowser &&
(function () {
const ua = window.navigator.userAgent
if (
(ua.indexOf('Android 2.') !== -1 || ua.indexOf('Android 4.0') !== -1) &&
ua.indexOf('Mobile Safari') !== -1 &&
ua.indexOf('Chrome') === -1 &&
ua.indexOf('Windows Phone') === -1
) {
return false
}
return window.history && typeof window.history.pushState === 'function'
})()
export function pushState (url?: string, replace?: boolean) {
saveScrollPosition()
// try...catch the pushState call to get around Safari
// DOM Exception 18 where it limits to 100 pushState calls
const history = window.history
try {
if (replace) {
// preserve existing history state as it could be overriden by the user
const stateCopy = extend({}, history.state)
stateCopy.key = getStateKey()
history.replaceState(stateCopy, '', url)
} else {
history.pushState({ key: setStateKey(genStateKey()) }, '', url)
}
} catch (e) {
window.location[replace ? 'replace' : 'assign'](url)
}
}
// 关键函数
function pushHash (path) {
if (supportsPushState) {
pushState(getUrl(path))
} else {
window.location.hash = path
}
}
而对于 React Router 来说,它的 push
函数也是先尝试使用 history.pushState
,不行再用 window.location.hash = xxx
来做兜底操作:
function push(to: To, state?: any) {
let nextAction = Action.Push;
let nextLocation = getNextLocation(to, state);
function retry() {
push(to, state);
}
warning(
nextLocation.pathname.charAt(0) === "/",
`Relative pathnames are not supported in hash history.push(${JSON.stringify(
to
)})`
);
if (allowTx(nextAction, nextLocation, retry)) {
let [historyState, url] = getHistoryStateAndUrl(nextLocation, index + 1);
try {
globalHistory.pushState(historyState, "", url);
} catch (error) {
// They are going to lose state here, but there is no real
// way to warn them about it since the page will refresh...
window.location.assign(url);
}
applyTx(nextAction);
}
}
而 pushState
是不会派发 hashchange
事件的,只有操作 window.location
,比如 window.locaiton.hash = xxx
才会派发 hashchange
事件,所以,当主应用点击 <Link>
的时候,实际上调用了 history.pushState
,并不会派发 hashchange
事件。那么子应用自然不知道路由变了,也就不会做页面的切换,最终我们会发现无论在主应用怎么切换路由,子应用的页面依然不会动。
为了验证这个想法,我们可以在控制台里手动监听一下 hashchange
事件是否触发了:
window.addEventListener('hashchange', (e) => console.log(e))
会发现如果我们直接点击 <Link>
,是完全没有打印任何东西的,而只有用 window.location.replace(window.location.href + '#' + 'taobao/about')
的方式才会打印 hash change event
:
我们也可以发现在这种情况下,子应用才能正常切换页面。
这就给我们解决这个问题的一个思路了:只要能 dispatch
一个 hashchange
事件,那么子应用就能正常切换路由了。 要实现这个效果,我们有两种方式:
手动 dispatch 事件
在做 JS 跳转时,可以手动 dispatch 一个 hashchange
事件:
history.push('/about')
const event = new Event('hashchange')
window.dispatchEvent(event)
使用 <a>
如果只是简单的 <Link>
,用 <a>
标签来替换就好了:
<ul>
<li>
<a href='/#/taobao/home'>淘宝首页</a>
</li>
<li>
<a href='/#/taobao/about'>淘宝关于</a>
</li>
</ul>
总结
最后做下总结,导致 “主应用切换路由时,子应用路由不切换” 的主要原因就是:子应用没有捕获到对应的 hashchange
事件,因此无法做路由切换。 对此,我们有两种方法来解决:
- 使用
dispatch(new Event('hashchange')
来手动产生一个事件,让子应用捕获到它,由此做路由切换 - 将
<Link>
转换为<a>
标签即可
这篇文章的代码都放在 这个仓库 qiankun-bigass-app 的 【routes】 分支上,需要的自行提取即可。如果你喜欢我的分享,可以来一波一键三连,点赞、在看就是我最大的动力,比心 ❤️
转载自:https://juejin.cn/post/7149433264598417445