前两天,业务方给我抛来一段代码,略去繁杂的逻辑,简化后的代码如下:
// 代码示例 1
import { Prompt, Link } from 'react-router-dom';
export const App = () => {
return (
<>
<Prompt message="跳转到另一个同微应用路由" />
<Link to="/detail">跳转到 detail </Link>
</>
)
}
在结合微前端框架 icestark 使用时,跳转到同一微应用的其他路由,会产生异常的效果:Prompt 弹窗了两次。
面对这个错误,我陷入了深深地沉思。接下来,我尝试解开这个错误的神秘面纱,在这个过程中,会涉及到: - React Router 的实现原理 - <Prompt />
的底层实现 - 以及微前端框架劫持路由后,面临的困境
我们以 BrowserHistory 为例:
// 代码示例 2
import { BrowserRouter, Route } from 'react-router-dom';
ReactDOM.render(
<BrowserRouter>
<Route exact path="/">
<Home />
</Route>
</BrowserRouter>
)
上面的代码会初始化一个 BrowserHistory 实例,并触发 BrowserHistory 的 listen 方法。这个方法做了两件事: 1. 监听全局 popstate 事件 2. 订阅 history 变化
这样,每当通过 history.push 或浏览器的前进后退变化路由(或触发 popstate 事件),从而动态渲染对应的页面组件。大致的流程如下图:
微前端框架(其运行时能力)与 React Router DOM 类似,本质是通过劫持 window.history 的 pushState 和 replaceState 方法,以及监听 popstate 和 hashChange 事件,并根据当前 URL 动态渲染匹配成功的微应用。 以微前端框架 icestark 为例,简化逻辑如下:
// 代码示例 3const originPush = window.history.pushState;const originReplace = window.history.replaceState;const urlChange = () => { // 根据 url 匹配相应的微应用}// 劫持 history 的 pushState 方法const hajackHistory = () => { window.history.pushState = (...rest) => { originPush.apply(window.history, [...rest]); urlChange(); } window.addEventListener('popstate', urlChange, false);}
### 但这样并不能解决全部问题
微应用是有独立路由的,当框架应用和微应用不共享同一个 history 实例的情况下。当框架应用切换路由,或其他微应用切换路由后,微应用如何能感知到路由变化呢?
比如,当通过框架应用的 history.push 切换同一个微应用的不同路由时,微应用没有并不会渲染出正确的页面。