123.路由重写-reroute
2024-04-10 06:40:27  阅读数 684
路由重写-reroute.jpeg

reroute 方法,没有调用start时进行加载app,调用loadApps方法,loadApps中准备加载应用,准备完毕后注册自定义事件callAllEventListeners

function reroute() {
      if (isStarted()) {
        appChangeUnderway = true;
        appsThatChanged = appsToUnload.concat(
          appsToLoad,
          appsToUnmount,
          appsToMount
        );
        return performAppChanges();
      } else {
        appsThatChanged = appsToLoad;
        return loadApps();
      }
      function loadApps() {
        return Promise.resolve().then(function () {
          var loadPromises = appsToLoad.map(toLoadPromise);
          return (
            Promise.all(loadPromises)
              .then(callAllEventListeners)
              .then(function () {
                return [];
              })
              .catch(function (err) {
                callAllEventListeners();
                throw err;
              })
          );
        });
      }
}

callAllEventListeners方法,遍历每个应用,给每个应用都添加callCapturedEventListeners事件监听,这边是操作的备份对象不会对浏览器中的事件进行重写。

  function callAllEventListeners() {
    pendingPromises.forEach(function (pendingPromise) {
      callCapturedEventListeners(pendingPromise.eventArguments);
    });
    callCapturedEventListeners(eventArguments);
  }

callCapturedEventListeners方法,遍历快照中重写的方法,之后监听到"hashchange", "popstate"时就会走我们自己定义的方法逻辑。

var routingEventsListeningTo = ["hashchange", "popstate"];
function callCapturedEventListeners(eventArguments) {
  var _this = this;
  if (eventArguments) {
    var eventType = eventArguments[0].type;
    if (routingEventsListeningTo.indexOf(eventType) >= 0) {
      capturedEventListeners[eventType].forEach(function (listener) {
        try {
          // The error thrown by application event listener should not break single-spa down.
          // Just like https://github.com/single-spa/single-spa/blob/85f5042dff960e40936f3a5069d56fc9477fac04/src/navigation/reroute.js#L140-L146 did
          listener.apply(_this, eventArguments);
        } catch (e) {
          setTimeout(function () {
            throw e;
          });
        }
      });
    }
  }
}

这个流程完毕之后,主应用调用了start方法后,就开始走performAppChanges相关逻辑。上篇文章有讲到过,主要是通过控制状态来达到激活和切换子应用的效果。

路由切换的时候怎么进行微应用加载的?

实现原理:通过监听和重写浏览器的hashchangepopstatepushStatereplaceState的方法

下面这段代码是默认执行的,说明一开始就对浏览器中的方法进行了注册和重写,hash模式的代码会重写hashchange方法,当使用history模式时,pushStatereplaceState事件被触发时不会触发 popstate事件,只有backforwardgo才可以触发。

pushState:只会向历史堆栈里面添加一个状态,不会更新页面,传了url不同源时会报错,不传则将其设置为当前文档的url。

replaceState:修改当前历史记录实体 ,同上,传了url不同源时会报错,不传则将其设置为当前文档的url。

//判断是否在浏览器环境
if (isInBrowser) {
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

  // 不要直接操作原有方法,在copy的方法上操作
  var originalAddEventListener = window.addEventListener;
  var originalRemoveEventListener = window.removeEventListener;
//重写pushState方法
  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
    //重写replaceState方法
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );
}

patchedUpdateState,记录跳转前和跳转后的url,如果urlBeforeurlAfter不同的时候才会重写路由,否则就只要进行pushStatereplaceState操作就会调用。

只有子应用启动了之后才手动触发popState事件,因为在没有启动子应用之前就可以使用默认的路由事件了。

dispatchEvent即为派发事件,实际就是我们在开始时监听了popstate事件,回调函数即reroute,因此pushstatereplacestate执行reroute的本质上是通过popstate来触发route

function patchedUpdateState(updateState, methodName) {
  return function () {
      //性能优化,只有当当前页面和要跳转的页面不一样时才创建createPopStateEvent
      //获取跳转前的url
    var urlBefore = window.location.href;
    var result = updateState.apply(this, arguments);
      //获取跳转后的url
    var urlAfter = window.location.href;
    if (!urlRerouteOnly || urlBefore !== urlAfter) {
      if (isStarted()) {
        window.dispatchEvent(
          createPopStateEvent(window.history.state, methodName)
        );
      } else {
        reroute([]);
      }
    }
    return result;
  };
}

createPopStateEvent方法,将重写后的popstate返回

function createPopStateEvent(state, originalMethodName) {
  var evt;
  try {
    evt = new PopStateEvent("popstate", {
      state: state,
    });
  } catch (err) {
    evt = document.createEvent("PopStateEvent");
    evt.initPopStateEvent("popstate", false, false, state);
  }
  evt.singleSpa = true;
  evt.singleSpaTrigger = originalMethodName;
  return evt;
}

为什么要重写pushStatereplaceState方法?

这篇文章对这个问题的解释我觉得是最合理的

除了在微前端框架中需要监听对应的导航事件外,在微前端框架外部我们也可以通过 addEventListener 的方式来注册 hashchangepopstate 事件,那么这样一来导航事件就会有多个,为了在实现对导航事件的控制,达到路由变化时对应的子应用能够正确的 卸载挂载,需要对 addEventListener 注册的 hashchangepopstate 进行拦截,并将对应的事件给存储起来,便于后续在特定的时候能够实现手动触发。

发现在挂载和卸载阶段都调用了callAllEventListeners方法,而callAllEventListeners又调用了callCapturedEventListeners方法,callCapturedEventListeners方法中是对"hashchange", "popstate"挨个进行手动调用。
这证明了加载子应用时通过reroute和路由控制不断地在加载和切换子应用。

callCapturedEventListeners方法源码参考上面