乐趣区

关于javascript:现代前端原生路由Navigation-API

Navigation API 是 Chrome 提出的一套导航 API,提供了操作和拦挡导航的能力,以及对应用程序的历史导航记录进行拜访。这为 window.history 和 window.location 提供了一个更有用的替代品,特地是 SPA 这种模式。目前该 API 只有 Chromium 内核的浏览器才反对。

Why

SPA:在用户与网站互动时动静重写其内容,而不是默认的从服务器加载全新页面的办法。

尽管基于 History API 曾经能够实现 SPA 了,然而 History API 过于简陋,不是专门为 SPA 量身定制的(早在 SPA 成为规范之前就开发进去了),在一些边界状况存在大量的问题,参见 W3C HTML History Issues。

如果开发人员在没有理解 History API 的状况下,想要基于 History API 实现相似 vue-router 路由守卫这种的性能时,会发现 window.onpopstate 只能监听到导航后退和后退的事件,无奈监听到 push 或 replace 事件。此外,在应用超链接标签 a 或表单标签 form 时,触发的导航都是不反对 SPA 的,像前端罕用的路由库 vue-router 或 react-router 都会提供本人的 Link 组件,用于实现 SPA 路由跳转。

在开源社区有曾经有一些针对 history 的封装了,例如:history、history.js,前者正是 react-router 的路由底层实现。而当初 Navigation API 提供一个全新的标准化客户端路由,专门为 SPA 定制,提供了残缺的操作和拦挡导航的能力,以及对应用程序的历史导航记录进行拜访。

疾速上手

要应用 Navigation API,首先在 window.navigation 上增加一个“navigate”事件监听。这个事件代表了页面上的所有同域导航事件,无论是用户点击链接,提交表单,或回退和后退。大多数状况下,在这个事件处理函数里能够重写浏览器对这些操作的默认行为。对于 SPA,这意味着能够让用户放弃在同一个页面上,并动静加载或更改站点的内容。

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
  </head>
  <body>
    <main>
      <ul>
        <li>
          <a href="subpage.html">subpage.html</a>
        </li>
        <li>
          <a href="#console">#console</a>
        </li>
        <li>
          <button onclick="history.pushState(null,'', '/subpage.html')">
            Go to subpage by history.pushState
          </button>
        </li>
        <li>
          <button onclick="history.back()">history.back()</button>
        </li>
        <li>
          <button onclick="location.reload()">location.reload()</button>
        </li>
        <li>
          <button onclick="location.href ='subpage.html'">
            Go to subpage by location.href
          </button>
        </li>
        <li>
          <a href="https://www.baidu.com">baidu</a>
        </li>
      </ul>
      <div id="console"></div>
    </main>
    <script type="module">
      navigation.addEventListener("navigate", (e) => {console.log(e);
        console.log('navigationType', e.navigationType); // 导航类型:"reload", "push", "replace", or "traverse"
        console.log('destination', e.destination); // 导航指标:{url: '', index:'', getState() {}}
        console.log('hashChange', e.hashChange); // 是否是锚点
        console.log('canTransition', e.canTransition); // 是否能够拦挡,即是否能够应用 transitionWhile

        if (e.hashChange || !e.canTransition) {
          // 疏忽锚点跳转
          return;
        }

        e.transitionWhile((async () => {e.signal.addEventListener("abort", () => {
              // 监听勾销事件
              const newMain = document.createElement("main");
              newMain.textContent =
                "Navigation was aborted, potentially by the browser stop button!";
              document.querySelector("main").replaceWith(newMain);
            });

            await delay(2000); // 成心提早 2 秒,测试用的

            // 动静加载指标页面内容
            const body = await (await fetch(e.destination.url, { signal: e.signal})
            ).text();
            const parser = new DOMParser();
            const doc = parser.parseFromString(body, "text/html");
            const title = doc.title;
            const main = doc.querySelector("main");

            document.title = title;
            document.querySelector("main").replaceWith(main);
          })());
      });

      navigation.addEventListener(
        "navigatesuccess",
        () => console.log("navigatesuccess") // 导航胜利事件(transitionWhile 失常响应));
      navigation.addEventListener(
        "navigateerror",
        (ev) => console.log("navigateerror", ev.error) // 导航失败事件(transitionWhile 异样响应));

      function delay(ms) {return new Promise((resolve) => setTimeout(resolve, ms));
      }
    </script>
  </body>
</html>

如上所示的示例,演示了超链接、history 和 location 操作导航的场景,它们都会触发 navigation 的 navigate 事件。通过事件的 navigationType 属性能够辨别导航类型,hashChange 示意是否是锚点跳转,destination 蕴含了跳转模板页面的信息,能够依据该信息动静的加载指标页面,从而实现 SPA。

导航事件

上文所示的 navigate 事件就是浏览器所有波及到地址变动时都会触发的导航事件,不仅包含 History API 对导航的操作,超链接标签 a、表单标签 form 的提交和 Location API 等对导航的操作都会触发 navigate 事件。在该事件的处理函数里能够对导航进行拦挡、重定向和勾销。

window.navigation.addEventListener("navigate", function (event: NavigateEvent) {console.log('navigationType', event.navigationType); // 导航类型:"reload", "push", "replace", or "traverse"
  console.log('destination', event.destination); // 导航指标:{url: '', index:'', getState() {}}
  console.log('hashChange', event.hashChange); // 是否是锚点
  console.log('canTransition', event.canTransition); // 是否能够拦挡,即是否能够应用 transitionWhile
});

导航类型信息

  • reload:刷新
  • push:关上新页面
  • replace:替换以后页面
  • traverse:导航后退或后退

导航指标信息次要蕴含了导航指标地址和状态信息

event.destination.url; // 指标地址
event.destination.getState(); // 相似 History API 的 state

除了以上 destination 和 navigationType 两个次要信息外,event 还提供了一些标识信息

  • hashChange:是否是锚点导航
  • canTransition:示意是否能够重写本次导航,实现 SPA 的自定义响应,除了跨域的导航无奈重写解决外,该标识个别都是为 true。

如果 canTransition 为 true,那么能够调用 event.transitionWhile 来重写导航行为,event.transitionWhile 会承受一个返回 Promise 的导航重写函数,这个 api 在下文的“导航解决”局部化具体介绍。依据导航重写函数的处理结果,还会触发胜利和失败事件。

  • navigatesuccess:导航重写函数返回的 Promise 响应胜利(resolve)时触发;
  • navigateerror:` 导航重写函数返回的 Promise 响应失败(reject)时触发。

导航解决

在 navigate 事件处理函数中,咱们能够依据须要对导航行为进行拦挡以阻止默认的导航行为,也能够自定义导航行为来笼罩默认的导航形式,从而实现 SPA。

阻止导航

通过调用 event.preventDefault() 能够勾销本次导航事件的默认行为,例如:点击超链接时默认会关上新页面。除了不能阻止浏览器的 后退和后退 行为外,其余导航变动事件都能够阻止。

自定义导航

当在 navigate 事件处理函数中调用 transitionWhile() 时,它告诉浏览器当初正在为新的导航指标筹备页面,浏览器不须要解决了(相当于阻止了默认行为,从而实现自定义的 SPA)。并且导航可能须要一些工夫,传递给 transitionWhile() 的 Promise 会通知浏览器导航须要多长时间。在这个处理过程中,咱们能够让浏览器显示导航的开始、完结或潜在的失败。例如,Chrome 浏览器会显示加载指示器,并容许用户与进行按钮互动。

navigation.addEventLisnter('navigate', () => {event.transitionWhile(async () => {
    // 显示导航指标加载动画
    const reponse = await (await fetch('...')).text();
    // 加载失败会触发 navigateerror 事件,否则触发 navigatesuccess 事件
    // 更新 DOM
  });
})
navigation.addEventLisnter('navigatesuccess', () => {// 暗藏导航指标加载动画})
navigation.addEventLisnter('navigateerror', () => {
  // 暗藏导航指标加载动画
  // 显示谬误页面 
})

须要留神的是,跨域的导航指标是不容许重写导航行为的。此外现有的地址更新模式还存在问题,浏览器默认解决导航时,会在指标地址的服务器响应后才会同步更新地址。然而新的 navigation API 在现阶段批改了这种行为,在重写了导航后,只有 navigate 事件处理函数执行完结,就会立即同步更新浏览器的地址,即便动静加载的内容还没响应回来。这会导致地址和页面显示内容不同步,因为异步加载指标页面时,以后页面还是显示上一个地址的内容,以后内容的一些资源的相对路径援用会出错。

下图所示是默认行为,点击链接后须要期待服务端响应后才会更新地址

下图是应用 transitionWhile 解决好的成果,在点击跳转后地址立即就产生了变动,但内容还是显示的旧地址页面。

目前最新的标准曾经调整了相干的实现,具体参考上面的一些探讨状况。截止到本文编写工夫,浏览器的实现还是旧的计划,所以本文还是按现有的实现去解说。

  • When should the URL change when using transitionWhile? #232
  • Will the current transitionWhile() design of updating the URL/history entry immediately meet web developer needs? #66
  • Make all same-document navigations sync #46
  • Worries about making all navigations async #19

导航勾销

因为自定义解决的导航是一个异步工作,在处理过程中用户可能点击页面上的其余拦挡,或者点击了浏览器的导航勾销、后退和后退按钮。为了解决这些状况,navigate 事件对象蕴含一个 AbortController 的信号属性 signal,通过这个信号属性能够监听导航勾销事件,你也能够将这个信号传给异步网络申请 fetch,以勾销网络申请工作,节俭带宽。

navigation.addEventLisnter('navigate', (event) => {event.signal.addEventListener("abort", () => {// ...});
  event.transitionWhile(async () => {await (await fetch('...', { signal: navigateEvent.signal})).text();});
})

导航操作

除了咱们移植的超链接标签 <a>, Location 和 History API 外,新的 Navigation API 也封装了导航操作方法。

  • navigation.navigate(url: string, options: { state: any, history: 'auto' | 'push' | 'replace'})

    关上指标地址页面,相等于 history.pushStatehistory.replaceState,然而反对跨域地址。

  • navigation.reload({state: any})

    刷新以后页面,相当于调用了 location.reload()

  • navigation.back()

    在导航会话历史中向后挪动一页,相当于 history.back()

  • navigation.forward()

    在导航会话历史中向前挪动一页,相当于 history.forward()

  • navigation.traverseTo(key: string)

    在导航会话历史记录中加载特定页面,相当于 history.go(),但区别在于传参不同,navigation 给每个导航会话设置了一个惟一标识,traverseTo 承受的参数正是该惟一标识,下文会介绍该惟一标识。

导航历史栈

在过来,History API 只提供了一个 history.length 来标识以后历史栈的大小,然而无法访问导航会话历史栈信息。而 Navigation API 提供了 currentEntry 和 entries 这两个 api 来拜访以后导航会话和会话历史栈。

每个导航会话对象的构造:

interface NavigationHistoryEntry extemds EventTarget {
  readonly id: string;
  readonly url: string;
  readonly key: string;
  readonly index: number;

  getState(): any;

  ondispose: EventHandler;
}
  • id:导航会话的惟一标识
  • url:导航会话的 URL 地址
  • key:在导航会话历史栈中的惟一标识

    id 与 key 的区别在于,key 标识是在栈中的惟一标识,id 是 NavigationHistoryEntry 实例的惟一标识。例如:调用 replace 或 reload 时并没有产生新的导航会话,但会生成新的 NavigationHistoryEntry,前后两个 NavigationHistoryEntry 实例的 key 雷同,但 id 不同。

    上文提到的 traverseTo 办法传参就是该 key 值。

  • index:批示该导航会话在历史栈的地位,默认从 0 开始
  • getState: 返回导航会话存储的状态,相似 history.state,详见下本介绍。
  • ondispose:监听 dispose 事件,在该导航会话从历史栈中删除时触发。

如下所示是一个简略示例演示历史栈的工作状况。

navigation.currentEntry // 以后属于首页 {index: 0, url: '/'}
navigation.navigate('/a', { state: { v: 1}}) // 关上 a 页面
navigation.navigate('/b', { state: { v: 2}}) // 关上 b 页面
navigation.navigate('/c', { state: { v: 3}}) // 关上 c 页面
navigation.back() // 返回上一页
navigation.currentEntry // 以后位于 b 页面 {index: 2, url: '/b'}
navigation.currentEntry.getState() // { v: 2}
navigation.entries() // 以后导航会话不肯定位于栈顶
/*
[{ index: 0, url: '/'},
  {index: 1, url: '/a'},
  {index: 2, url: '/b'},
  {index: 3, url: '/c'},
]
*/

导航状态

相似 history.state,navigation 在每个导航会话提供了 getState 办法来获取以后导航会话的缓存状态,即便刷新了浏览器该状态仍能复原。

navigation.currentEntry.getState() // 以后导航会话的缓存状态
navigation.entries().map(entry => entry.getState()) // 所有导航会话历史的缓存状态

导航会话的状态是在调用 navigation.navigate(url: string, options: { state: any}) 时设置的。如果要更新以后导航会话的状态,能够调用 navigation.updateCurrentEntry(options: { state: any})。,这在过来应用 History API 时,并没有那么不便的相似 API 可用。

在 SPA 里,导航状态还是有很大的利用场景,在过来因为没有方便使用的 API,很多时候须要应用其余计划来代替。例如:有些开发者须要记住页面状态时,会应用全局状态治理来存储。晚期 redux 风行时,其官网的示例演示了如何在全局缓存页面状态。但这么做,在一些路由组件在导航历史同时呈现时,会呈现状态抵触。

假如一个 SPA 存在一个列表页面 /list 和一个具体页面 /detail,列表页面的每一项点击后关上详情页面,详情页面存在更多链接又能够关上新的列表页面。这样的话,在导航历史栈中就存在两个列表页面,但这两个列表页面的页面状态应该是不一样的。如果依照全局状态治理的形式解决,那么会两个列表页面的页面状态就会存在抵触,要么是新的列表笼罩了就的列表状态,要么就是新的列表谬误的应用了旧列表的页面状态。在这种状况下,咱们应该应用”导航状态“来缓存页面状态。

总结

Navigation API 综合封装了浏览器的导航能力,提供了中心化的监听事件和不便的自定义导航实现形式,而且补充提供了导航会话历史的拜访和状态治理,这些大大简化了 SPA 的实现。

参考文献

  • https://github.com/WICG/navig…
  • https://caniuse.com/mdn-api_n…
  • Modern client-side routing: the Navigation API
  • Feature: Navigation API
退出移动版