Fancy Front End Fancy Front End
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)

Jonsam NG

让有意义的事变得有意思,让有意思的事变得有意义
  • 开始上手
  • 基础
  • 调度器(Scheduler)
  • 更新器(Updater)
  • 渲染器(Render)
  • 更新周期
  • hooks 原理
  • 总结
  • 📙 React源码漂流记
  • 开始上手
  • 基础
  • reactivity
  • runtime-core
  • runtime-dom
  • Awesome Web
  • Awesome NodeJS
话题
  • 导航
  • Q&A
  • 幻灯片
  • 关于
  • 分类
  • 标签
  • 归档
博客 (opens new window)
GitHub (opens new window)
  • 开始上手
  • Plan 计划
  • typescript-utility

  • single-spa源码

    • 开始阅读
    • app与应用管理

    • lifecycles与生命周期管理

    • navigation与路由管理

      • 本章概要
      • navigation-events 路由监听
        • 目录
        • 路由监听
        • urlReroute
        • patchedUpdateState
        • navigateToUrl
        • createPopStateEvent
      • reroute 根据路由更新应用状态
    • parcel组件

    • 其他

    • single-spa-react

  • qiankun源码

  • webpack

  • axios

  • solid

  • vite源码

  • jquery源码

  • snabbdom

  • am-editor

  • html2canvas

  • express

  • acorn源码

  • immutable.js源码

  • web
  • single-spa源码
  • navigation与路由管理
jonsam
2022-04-18
目录

navigation-events 路由监听

本节探讨 single 中路由管理部分路由的监听与代理的内容。我们知道,微应用需要根据一定的规则匹配到相应的路由,并根据路由去挂载和卸载微应用。从整体上来看,s-spa 需要完成这几件事情:路由监听、路由匹配、应用状态更新(mount 或者 unmount)。作为 s-spa 中独立的一部分,这部分具有一定的复杂度。

# 目录

  • 目录
  • 路由监听
  • urlReroute
  • patchedUpdateState
  • navigateToUrl
  • createPopStateEvent

从路由监听的角度来看我们不得不考虑以下的诸多问题:

  • hash 路由和 history 路由
  • url 路由变化和直接操作 history 导致 url 变化

# 路由监听

框架初始化时执行,监听 window 上 hashchange 和 popstate 事件,分别在 url hash 变化和 popstate 时触发;代码 history.pushState 和 history.replaceState,在两者执行时比较 url 是否变化。如果 url 变化将执行 reroute,调整应用匹配和应用状态更新。

if (isInBrowser) {
  // 监听 hashchange 和 popstate 事件
  // 这里不一定是原生的 addEventListener ,因为允许被代理
  // We will trigger an app change for any routing events.
  window.addEventListener("hashchange", urlReroute);
  window.addEventListener("popstate", urlReroute);

  // Monkeypatch addEventListener so that we can ensure correct timing
  const originalAddEventListener = window.addEventListener;
  const originalRemoveEventListener = window.removeEventListener;
  // 代理 addEventListener 和 removeEventListener
  window.addEventListener = function (eventName, fn) {
    // 注意,以下代码在 addEventListener 时执行一次,在 listener 被回调时不会再执行
    if (typeof fn === "function") {
      // 如果是需要监听的路由事件,且未在 capturedEventListeners[eventName] 上注册
      if (
        routingEventsListeningTo.indexOf(eventName) >= 0 &&
        !find(capturedEventListeners[eventName], (listener) => listener === fn)
      ) {
        // 将 listener 注册到 capturedEventListeners
        capturedEventListeners[eventName].push(fn);
        // 注意:收集到 listeners 之后就返回了,由 s-spa 接管了 listeners 的调用
        return;
      }
    }

    return originalAddEventListener.apply(this, arguments);
  };

  window.removeEventListener = function (eventName, listenerFn) {
    if (typeof listenerFn === "function") {
      // 如果在 capturedEventListeners 中注册过此 listener,则将之删除
      if (routingEventsListeningTo.indexOf(eventName) >= 0) {
        capturedEventListeners[eventName] = capturedEventListeners[
          eventName
        ].filter((fn) => fn !== listenerFn);
        return;
      }
    }

    return originalRemoveEventListener.apply(this, arguments);
  };
  // 代理 history.pushState 和 history.replaceState
  // patchedUpdateState 需要比较 url 是否变化
  window.history.pushState = patchedUpdateState(
    window.history.pushState,
    "pushState"
  );
  window.history.replaceState = patchedUpdateState(
    window.history.replaceState,
    "replaceState"
  );
  // 如果此代码被执行了两次则出现异常
  if (window.singleSpaNavigate) {
    console.warn(
      formatErrorMessage(
        41,
        __DEV__ &&
          "single-spa has been loaded twice on the page. This can result in unexpected behavior."
      )
    );
  } else {
    /* For convenience in `onclick` attributes, we expose a global function for navigating to
     * whatever an <a> tag's href is.
     */
    // 便于调用 navigateToUrl 进行导航
    window.singleSpaNavigate = navigateToUrl;
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69

这个函数的主要作用如下:

  • 路由监听:hashchange、popstate、history.pushState 和 history.replaceState。
  • 代理 window.addEventListener 和 window.removeEventListener 手机 hashchange 和 popstate 的 listener。
  • 将 singleSpaNavigate 挂载到 window.singleSpaNavigate。

# urlReroute

由 hashchange 和 popstate 引起的 url 变化,执行 reroute。

function urlReroute() {
  // url 已知变化,直接 reroute
  reroute([], arguments);
}
1
2
3
4

# patchedUpdateState

由 history.pushState 和 history.replaceState 引起的 state 变化,先比较 url 是否变化,在执行 reroute。

function patchedUpdateState(updateState, methodName) {
  // 从 history.pushState 和 replace.replaceState,需比对 url 是否变化
  return function () {
    const urlBefore = window.location.href;
    // 调用原生函数
    const result = updateState.apply(this, arguments);
    const urlAfter = window.location.href;
    // 只有不是 urlRerouteOnly,且 url 发生变化
    if (!urlRerouteOnly || urlBefore !== urlAfter) {
      if (isStarted()) {
        // fire an artificial popstate event once single-spa is started,
        // so that single-spa applications know about routing that
        // occurs in a different application
        // 通过事件系统仿造一个 popsState 事件,以触发 reroute,并且能够使所有微应用监听到变化
        window.dispatchEvent(
          createPopStateEvent(window.history.state, methodName)
        );
      } else {
        // do not fire an artificial popstate event before single-spa is started,
        // since no single-spa applications need to know about routing events
        // outside of their own router.
        // s-spa 还未 start,不需要以事件的形式进行通知,直接执行 reroute
        // 注意:即使还没有 start,仍然需要 reroute,因为 reroute 会针对 start 情况做处理
        reroute([]);
      }
    }

    return result;
  };
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30

注意

如果框架已经 start,则需要以事件的方式引发 reroute,因为需要通知微应用路由的变化。这里使用 createPopStateEvent 仿造 popState 事件。

# navigateToUrl

Use this utility function to easily perform url navigation between registered applications without needing to deal with event.preventDefault(), pushState, triggerAppChange(), etc.

/**
 * see https://single-spa.js.org/docs/api/#navigatetourl
 * 不使用任何框架导航到目标 url,同时触发应用的更新 triggerAppChange
 */
export function navigateToUrl(obj) {
  let url;
  // obj 为 url
  if (typeof obj === "string") {
    url = obj;
  } else if (this && this.href) {
    // obj 为 a 标签
    url = this.href;
  } else if (
    // object 为 ClickEvent
    obj &&
    obj.currentTarget &&
    obj.currentTarget.href &&
    obj.preventDefault
  ) {
    url = obj.currentTarget.href;
    obj.preventDefault();
  } else {
    throw Error(
      // ......
    );
  }
  // 将 currentUrl 和 url 分别生成 a 标签
  const current = parseUri(window.location.href);
  const destination = parseUri(url);
 
  if (url.indexOf("#") === 0) {
    // url 是 hash 
    window.location.hash = destination.hash;
  } else if (current.host !== destination.host && destination.host) {
    if (process.env.BABEL_ENV === "test") {
      return { wouldHaveReloadedThePage: true };
    } else {
      // 改变了 host
      window.location.href = url;
    }
  } else if (
    destination.pathname === current.pathname &&
    destination.search === current.search
  ) {
    // pathname 和 search 没有变
    window.location.hash = destination.hash;
  } else {
    // different path, host, or query params
    window.history.pushState(null, null, url);
  }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51

navigateToUrl 方法不依赖于其他框架的路由组件,提供导航的功能,同时帮助我们触发 reroute。

# createPopStateEvent

createPopStateEvent 创建一个 popState 事件。在 history.pushState 和 history.replaceState 被调用且 url 发生变化时,主动发出一个 popState 事件,以使微应用可以监听到此事件而得知路由发生了变化。

function createPopStateEvent(state, originalMethodName) {
  // https://github.com/single-spa/single-spa/issues/224 and https://github.com/single-spa/single-spa-angular/issues/49
  // We need a popstate event even though the browser doesn't do one by default when you call replaceState, so that
  // all the applications can reroute. We explicitly identify this extraneous event by setting singleSpa=true and
  // singleSpaTrigger=<pushState|replaceState> on the event instance.
  let evt;
  try {
    // IE 浏览器不支持 PopStateEvent(), see https://caniuse.com/mdn-api_popstateevent_popstateevent
    evt = new PopStateEvent("popstate", { state });
  } catch (err) {
    // IE 11 compatibility https://github.com/single-spa/single-spa/issues/299
    // https://docs.microsoft.com/en-us/openspecs/ie_standards/ms-html5e/bd560f47-b349-4d2c-baa8-f1560fb489dd
    evt = document.createEvent("PopStateEvent");
    // nitializes the properties of a PopStateEvent object.Available only in IE10, IE11, and EdgeHTML Mode (All Versions).
    evt.initPopStateEvent("popstate", false, false, state);
  }
  // 区分其他 popState 事件
  evt.singleSpa = true;
  evt.singleSpaTrigger = originalMethodName;
  return evt;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21

在外界的应用中,同样可以通过监听 hashchange 和 popState 事件以得知 url 发生了变化,而不用额外封装 history.pushState 和 history.replaceState 。

参考:

  • Events | single-spa (opens new window)
编辑 (opens new window)
上次更新: 2022/09/06, 14:25:16
本章概要
reroute 根据路由更新应用状态

← 本章概要 reroute 根据路由更新应用状态→

最近更新
01
渲染原理之组件结构与 JSX 编译
09-07
02
计划跟踪
09-06
03
开始上手
09-06
更多文章>
Theme by Vdoing | Copyright © 2022-2022 Fancy Front End | Made by Jonsam by ❤
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式