likes
comments
collection
share

Qiankun实践——彻底解决无法切换路由的问题

作者站长头像
站长
· 阅读数 96

前言

哈喽,大家好,我是海怪。

今天想跟大家分享一个在使用 Qiankun 中遇到的问题,相信不少人也会遇到过,那就是:在主应用切换路由时,无法切换对应子应用的路由。

好吧,这句话听着就不像人话。不过,如果你遇过同样的问题,那么应该能够理解我的意思。不理解的同学也别慌,下面我来慢慢跟大家解释一下这个现象。

这篇文章的代码都放在了 这个仓库 qiankun-bigass-app 的 【routes】 分支上,需要的自行提取即可。废话不多说,我们现在就开始吧。

项目背景

我是怎么发现这个问题的呢?下面来说一下我的项目背景:

当时,我到新部门的时候,这里一共有好几个项目(项目的名字都用了化名,大家看个意思就可以了):

Qiankun实践——彻底解决无法切换路由的问题

其中,绿色的项目都是一些比较小且专一的管理后台,而红色的项目则是一个功能更广且泛的管理后台。面对这么分散的项目,我们希望通过 Qiankun 的微前端架构能力来管理这些项目。具体来说就是 把红色项目作为主应用基座,绿色的项目作为子应用,把部分子应用的功能慢慢集成到红色的大平台上。

不过在实现的时候,我们并不是直接开 4 个 Tab,然后一个 Tab 对应一个项目来集成。因为这样做的话,用户就要点击很深入才能找到对应的功能,用户更需要的是将所有功能平铺开来,而且希望有取舍地集成:只将部分重要的功能集成到大平台上,剩余的功能由各自项目来承担

Qiankun实践——彻底解决无法切换路由的问题

对于页面来说,大概是长这样的:

Qiankun实践——彻底解决无法切换路由的问题

发现问题

为了实现上面这样的效果,我们希望主应用侧边栏的每一个 Tab 对应一个路由,当点击这个路由的时候,就会加载对应的子应用,以及该子应用对应的那个路由页面。

举个例子:

  1. 点击 “用户信息”,主应用路由会切换到 /userMicroApp/userinfo
  2. 然后主应用加载 UserMicroApp 微应用
  3. 微应用 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>

Qiankun实践——彻底解决无法切换路由的问题

神奇的地方来了:你会发现点击主应用的 <Link> 并没有任何作用,而点击 Taobao <nav> 里的 <Link> 是可以正常切换。这就是标题里说的 “主应用切换路由时,不更新子应用路由” 的问题。 具体可以看 这个 Issue

分析原因

为什么主应用切换了路由,子应用的路由不更新呢?面对这种难以排查的问题,我们要把整个链路理清楚:

  • 主应用点击 <Link>
  • 触发事件
  • 主应用捕获到了路由变更事件
  • 主应用切换了页面 / 加载子应用
  • 子应用也捕获到了路由变更事件
  • 子应用切换页面

我们先在主应用里添加一些简单的路由页面,以此检查主应用能否做切换,而显然主应用是能够正常切换页面的:

Qiankun实践——彻底解决无法切换路由的问题

上面的 “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 会监听两种模式下的不同事件,以此又来做状态变更、寻找页面组件、更新页面等等操作。整体如下图所示:

Qiankun实践——彻底解决无法切换路由的问题

解决问题

现在回到我们这个例子。

对于 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

Qiankun实践——彻底解决无法切换路由的问题

我们也可以发现在这种情况下,子应用才能正常切换页面。

这就给我们解决这个问题的一个思路了:只要能 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
评论
请登录