「實踐篇」解決微前端 single-spa 專案中 Vue 和 React 路由跳轉問題

我不是大熊哦發表於2022-04-05

前言

本文介紹的是在做微前端 single-spa 專案過程中,遇到的 Vue 子應用和 React 子應用互相跳轉路由時遇到的問題。

專案情況:single-spa 專案,基座用的是 React,目前是2個子應用一個 Vue 一個 React。路由方案是 Vue Router,React Router + history。

有互動場景是從 Vue 子應用跳轉到 React 子應用,或者從 React 子應用跳轉到 Vue 子應用,因此遇到了問題。

遇到的問題

image.png

結合專案訴求和遇到的問題,大家可以先思考一下有什麼解決方案~

------------------------------分割線------------------------------

解決的方案

主要是要處理好以下2個因素:

  • 正常觸發當前頁面的路由鉤子
  • 正常傳路由引數到目的頁面
    我一開始嘗試去查閱社群文章和看部分原始碼,看是否有什麼特殊方式自主去觸發路由鉤子,等鉤子處理完成之後再跳轉去目的頁面(跳去 Vue 用 Vue Router,跳去 React 用 React Router)

但看原始碼下來發現,想要觸發 Prompt 還是需要呼叫 history.push 觸發流程,想要觸發 Vue Router 導航守衛還是需要呼叫 VueRouter.push 觸發流程

所以結合這兩點我整出了解決方案,已使用在專案當中,下面是封裝的全域性路由跳轉工具:

window.micro = {
  // 子應用,會在相應應用掛載完成後設定
  apps: { vue: null, react: null },
  history: {
    push: (location, onComplete, onAbort) => {
      const url = typeof location === 'string' ? location : location.path;
      // 判斷是哪個子應用
      const currentIsReact = isReactApp();
      const nextIsReact = isReactApp(`#${url}`);
      // 處理路由引數
      let state = {};
      let query = {};
      let name = '';
      if (typeof location !== 'string') {
        state = location.params || {};
        query = location.query || {};
        name = location.name || '';
      }
      if (!currentIsReact && nextIsReact) {
        // vue 跳 react:先用 vue-router 跳,在跳完的回撥裡再用 history 跳
        const reactHistoryHandle = () => {
          onComplete?.();
          history.push(`#/temp?t=${Math.random()}`);
          history.replace({ state, pathname: url, search: setQueryStringArgs(query) });
          // 因為跳多了1次 vue-router,所以 back 一下
          window.micro.apps.vue2.$router.back();
        };
        window.micro.apps.vue.$router.push(name ? { name, params: state, query } : { path: url, query }, reactHistoryHandle, onAbort);
      } else if (currentIsReact && !nextIsReact) {
        // react 跳 vue:先用 history 跳臨時路由,再用 vue-router 跳,要配合 history.listen 做處理
        react2vue = () => {
          window.micro.apps.vue.$router.push(name ? { name, params: state, query } : { path: url, query }, onComplete, onAbort);
        };
        history.push('/temp_react_to_vue');
      } else if (currentIsReact && nextIsReact) {
        // react 跳 react:沒有特殊,正常用 history 跳
      } else {
        // vue 跳 vue:沒有特殊,正常用 vue-router 跳
      }
    },
  },
};

配合的監聽和工具函式:

// 處理 react 跳 vue的情況

let react2vue = null;
history.listen((location, action) => {
  // 處理在臨時路由的下一個路由,要返回上一個路由時,需要跳過臨時路由
  if (location.pathname === '/temp_react_to_vue' && action === 'POP') {
    history.goBack();
  } else if (location.pathname === '/temp_react_to_vue') {
    // 在這裡跳去真實的 vue-router 路由
    react2vue?.();
    react2vue = null;
  }
});


// 工具函式
function isReactApp(hash = location.hash) {
  // 實際根據自己微服務專案的子應用情況判斷
  return hash === '' || hash === '#/' 
    || ['#/list', '#/read/', '#/compose', '#/login'].some(router => hash.startsWith(router))
    ;
}
// 把query引數變成query字串,如 {a:1, b:2} 返回 ?a=1&b=2
function setQueryStringArgs(args) {
  let str = '';
  args = args || {};
  for (const [key, value] of Object.entries(args)) {
    str += `${key}=${encodeURIComponent(String(value))}`;
  }
  return str ? `?${str}` : '';
}

總結

image.png

這是我在實際專案中使用的方案,如有更優雅更好的方案,希望在評論區和我討論~

收穫 / 小彩蛋

因為之前已經對 Vue Router 原理和原始碼比較熟悉了,所以這次藉著這個問題,主要是去了解了 React Router 的 Prompt 元件的實現,這裡簡單總結一下:

image.png

相關文章