1
Original: https://github.com/zhbhun/blog/issues/4

The Navigation API is a set of navigation APIs proposed by Chrome, which provides the ability to operate and intercept navigation, as well as access the historical navigation records of the application. This provides a more useful replacement for window.history and window.location, especially in SPA mode. Currently this API is only supported by Chromium-based browsers.

compatibility

Why

SPA: Dynamically rewrite the content of a website as the user interacts with it, rather than the default method of loading an entirely new page from the server.

Although SPA can already be implemented based on the History API , the History API is too simple and not specially tailored for SPA (it was developed long before SPA became a standard), and there are a lot of problems in some edge cases, see W3C HTML History Issues .

If developers want to implement functions like vue-router routing guard based on the History API without knowing the History API, they will find that window.onpopstate can only monitor the navigation forward and backward events. Unable to listen to push or replace events. In addition, when using the hyperlink tag a or the form tag form , the triggered navigation does not support SPA, like the common routing library vue-router or react-router in the front end. Provide your own Link component for implementing SPA routing jumps.

There are already some encapsulations for history in the open source community, such as history , history.js , the former is the underlying implementation of react-router routing. And now the Navigation API provides a new standardized client-side routing, tailored specifically for SPAs, providing complete capabilities for manipulating and intercepting navigation, as well as accessing the application's historical navigation records.

Get started quickly

To use the Navigation API, first add a "navigate" event listener on window.navigation . This event represents all navigation events on the page, whether the user clicks a link, submits a form, or goes back and forth. In most cases, the browser's default behavior for these operations can be overridden in this event handler. For a SPA, this means keeping the user on the same page and dynamically loading or changing the content of the site.

 <!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>

The example shown above demonstrates the scenario of hyperlink, history, and location action navigation, all of which trigger the navigation event of navigation. The navigation type can be distinguished by the navigationType property of the event, hashChange indicates whether it is an anchor jump, destination contains the information of the jump template page, and the target page can be dynamically loaded according to the information to realize SPA.

Navigation events

The navigate event shown above is all the navigation events that the browser will trigger when the address changes, including not only the operations of the History API on navigation, but also the operations of the hyperlink tag a, the submission of the form tag form, and the Location API. Trigger the navigate event. Navigation can be intercepted, redirected and canceled in the event handler.

 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
});

Navigation Type Information

  • reload : refresh
  • push : open a new page
  • replace : replace the current page
  • traverse : Navigate forward or backward

Navigation target information mainly includes navigation target address and status information

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

In addition to the above two main information of destination and navigationType, event also provides some identification information

  • hashChange: whether it is anchor navigation
  • canTransition: Indicates whether the navigation can be rewritten to realize the custom response of the SPA. Except that the cross-domain navigation cannot be rewritten, this flag is generally true.

If canTransition is true, then event.transitionWhile can be called to rewrite the navigation behavior, event.transitionWhile will accept a navigation rewrite function that returns a Promise, this api is described in the "Navigation Handling" section below Details. Success and failure events are also fired based on the processing results of the navigation rewrite function.

  • navigatesuccess : Triggered when the Promise response returned by the navigation rewrite function is successful (resolve);
  • navigateerror : `Fired when the Promise response returned by the navigation override function fails (reject).

Navigation handling

In the navigate event handler function, we can intercept the navigation behavior as needed to prevent the default navigation behavior, or we can customize the navigation behavior to override the default navigation method to implement SPA.

block navigation

By calling event.preventDefault() the default behavior of this navigation event can be canceled, for example, a new page will be opened by default when a hyperlink is clicked. In addition to not blocking the browser's forward and backward behavior, other navigation change events can be blocked.

custom navigation

When called in the navigate event handler transitionWhile() , it informs the browser that the page is now being prepared for a new navigation target, and the browser does not need to deal with it (equivalent to preventing the default behavior, thus implementing a custom SPA). And navigation may take some time, the Promise passed to transitionWhile() will tell the browser how long the navigation will take. During this process, we can have the browser display the start, end, or potential failure of the navigation. For example, the Chrome browser displays a loading indicator and allows the user to interact with the stop button.

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

It should be noted that cross-domain navigation targets are not allowed to override navigation behavior. In addition, there is still a problem with the existing address update mode. When the browser handles navigation by default, the address will be updated synchronously after the server of the target address responds. However, the new navigation API modifies this behavior at this stage. After rewriting the navigation, as long as the navigate event handler execution ends, the browser's address will be updated synchronously, even if the dynamically loaded content Haven't responded yet. This will cause the address and page display content to be out of sync, because when the target page is loaded asynchronously, the current page still displays the content of the previous address, and the relative path reference of some resources of the current content will be wrong.

The following figure shows the default behavior. After clicking the link, you need to wait for the server to respond before updating the address.

1

The following picture is the effect processed by transitionWhile. After clicking the jump, the address changes immediately, but the content is still the old address page displayed.

2

The latest specification has adjusted the relevant implementation, please refer to the following discussion for details. As of the writing time of this article, the implementation of the browser is still an old solution, so this article will explain it according to the existing implementation.

Navigation Cancel

Since custom-handled navigation is an asynchronous task, the user may have clicked on other blocks on the page during processing, or clicked the browser's navigation cancel, forward, and back buttons. In order to handle these situations, the navigate event object contains a signal property of signal , through which you can listen to the navigation cancellation event, you can also pass this signal to the asynchronous network request fetch to cancel the network request task , saving bandwidth.

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

Navigation action

In addition to our ported hyperlink tags <a> , Location and History APIs, the new Navigation API also encapsulates navigation operation methods.

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

    Open the target address page, which is equivalent to history.pushState and history.replaceState , but supports cross-domain addresses.

  • navigation.reload({ state: any })

    Refreshing the current page is equivalent to calling location.reload()

  • navigation.back()

    Move back one page in the navigation session history, equivalent to history.back()

  • navigation.forward()

    Move forward one page in the navigation session history, equivalent to history.forward()

  • navigation.traverseTo(key: string)

    Loading a specific page in the navigation session history is equivalent to history.go() , but the difference is that the parameters are different. Navigation sets a unique identifier for each navigation session, and the parameter accepted by traverseTo is the unique identifier. This unique identifier is described.

Navigate the history stack

In the past, the History API only provided a history.length to identify the size of the current history stack, but there was no way to access the navigation session history stack information. The Navigation API provides two APIs, currentEntry and entries, to access the current navigation session and the session history stack.

The structure of each navigation session object:

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

  getState(): any;

  ondispose: EventHandler;
}
  • id: the unique identifier of the navigation session
  • url: URL address of the navigation session
  • key: unique identifier in the navigation session history stack

    The difference between id and key is that the key identifier is the unique identifier in the stack, and the id is the unique identifier of the NavigationHistoryEntry instance. For example: when replace or reload is called, a new navigation session is not generated, but a new one NavigationHistoryEntry is generated. The keys of the two NavigationHistoryEntry instances before and after are the same, but the ids are different.

    The above-mentioned traverseTo method parameter is the key value.

  • index: indicates the position of the navigation session in the history stack, starting from 0 by default
  • getState: Returns the state of the navigation session storage, similar to history.state, see the following introduction for details.
  • ondispose: Listen to the dispose event, triggered when the navigation session is removed from the history stack.

The following is a simple example to demonstrate the working of the history stack.

 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' },
]
*/

Navigation state

Similar to history.state , navigation provides the getState method for each navigation session to get the cached state of the current navigation session, which can be restored even if the browser is refreshed.

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

The state of the navigation session is set when navigation.navigate(url: string, options: { state: any }) is called. If you want to update the state of the current navigation session, you can call navigation.updateCurrentEntry(options: { state: any })。 , which was not so convenient when using the History API in the past.

In SPA, the navigation state still has a large application scenario. In the past, because there was no easy-to-use API, other solutions were often needed instead. For example, when some developers need to remember page state, they use global state management to store it. When redux was popular in the early days, its official example demonstrated how to cache page state globally. But doing this, there will be state conflicts when some routing components appear at the same time in the navigation history.

Suppose a SPA has a list page /list and a detail page /detail , each item on the list page will be clicked to open the details page, and if there are more links in the details page, you can open a new list page. In this case, there are two list pages in the navigation history stack, but the page states of these two list pages should be different. If it is handled in the way of global state management, the page states of the two list pages will conflict, either the new list overwrites the previous list state, or the new list mistakenly uses the page state of the old list. In this case, we should use "navigation state" to cache the page state.

Summarize

The Navigation API comprehensively encapsulates the browser's navigation capabilities, provides centralized monitoring of events and a convenient way to implement custom navigation, and supplements access to navigation session history and state management, which greatly simplifies the implementation of SPA.

At present, the Chrome 102 version already supports the Navigation API, and other browsers do not yet support it. I am trying to write a related polyfill - navigation-polyfill to be compatible with other browsers

references


zhbhun
1k 声望5 粉丝