问题

前两天,业务方给我抛来一段代码,略去繁杂的逻辑,简化后的代码如下:

// 代码示例 1
import { Prompt, Link } from 'react-router-dom';

export const App = () => {
  return (
    <>
      <Prompt message="跳转到另一个同微应用路由" />
      <Link to="/detail">跳转到 detail </Link>
    </>
  )
}

在结合微前端框架 icestark 使用时,跳转到同一微应用的其他路由,会产生异常的效果:Prompt 弹窗了两次

https://img.alicdn.com/imgextra/i2/O1CN01LRBZex1KyIeJBcoOP_!!6000000001232-1-tps-1694-546.gif

面对这个错误,我陷入了深深地沉思。接下来,我尝试解开这个错误的神秘面纱,在这个过程中,会涉及到: ​ - React Router 的实现原理 - <Prompt /> 的底层实现 - 以及微前端框架劫持路由后,面临的困境

React Router DOM 是怎么实现单页应用路由的

我们以 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 事件),从而动态渲染对应的页面组件。大致的流程如下图:

https://img.alicdn.com/imgextra/i1/O1CN01Po6aS21ltFuhLwuLj_!!6000000004876-2-tps-462-743.png

微前端的路由劫持逻辑

微前端框架(其运行时能力)与 React Router DOM 类似,本质是通过劫持 window.historypushStatereplaceState 方法,以及监听 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 切换同一个微应用的不同路由时,微应用没有并不会渲染出正确的页面。

https://img.alicdn.com/imgextra/i4/O1CN01FgHuhN1HVUtU8GtsR_!!6000000000763-1-tps-2036-734.gif