Định tuyến phía máy khách hiện đại: Navigation API

Chuẩn hoá hoạt động định tuyến phía máy khách thông qua một API hoàn toàn mới, giúp đại tu hoàn toàn việc xây dựng các ứng dụng một trang.

Browser Support

  • Chrome: 102.
  • Edge: 102.
  • Firefox: 147.
  • Safari: 26.2.

Source

Ứng dụng một trang (SPA) được xác định bằng một tính năng cốt lõi: viết lại nội dung một cách linh hoạt khi người dùng tương tác với trang web, thay vì phương thức mặc định là tải hoàn toàn các trang mới từ máy chủ.

Mặc dù SPA có thể mang đến cho bạn tính năng này thông qua History API (hoặc trong một số trường hợp, bằng cách điều chỉnh phần #hash của trang web), nhưng đây là một API vụng về được phát triển từ rất lâu trước khi SPA trở thành tiêu chuẩn và web đang rất cần một phương pháp hoàn toàn mới. Navigation API là một API được đề xuất để đại tu hoàn toàn không gian này, thay vì chỉ cố gắng vá các điểm chưa hoàn thiện của History API. (Ví dụ: Khôi phục trạng thái cuộn đã vá API Nhật ký thay vì cố gắng tạo lại API này.)

Bài đăng này mô tả Navigation API ở cấp độ cao. Để đọc đề xuất kỹ thuật, hãy xem Báo cáo nháp trong kho lưu trữ WICG.

Ví dụ về cách sử dụng

Để sử dụng Navigation API, hãy bắt đầu bằng cách thêm một trình nghe "navigate" vào đối tượng navigation chung. Về cơ bản, sự kiện này được tập trung: sự kiện này sẽ kích hoạt cho tất cả các loại thao tác điều hướng, cho dù người dùng thực hiện một hành động (chẳng hạn như nhấp vào đường liên kết, gửi biểu mẫu hoặc quay lại và chuyển tiếp) hay khi thao tác điều hướng được kích hoạt theo phương thức lập trình (tức là thông qua mã của trang web). Trong hầu hết các trường hợp, sự kiện này cho phép mã của bạn ghi đè hành vi mặc định của trình duyệt đối với thao tác đó. Đối với SPA, điều đó có nghĩa là giữ người dùng trên cùng một trang và tải hoặc thay đổi nội dung của trang web.

Một NavigateEvent được truyền đến trình nghe "navigate". Trình nghe này chứa thông tin về hoạt động điều hướng, chẳng hạn như URL đích đến, đồng thời cho phép bạn phản hồi hoạt động điều hướng ở một vị trí tập trung. Một trình nghe "navigate" cơ bản có thể trông như sau:

navigation.addEventListener('navigate', navigateEvent => {
  // Exit early if this navigation shouldn't be intercepted.
  // The properties to look at are discussed later in the article.
  if (shouldNotIntercept(navigateEvent)) return;

  const url = new URL(navigateEvent.destination.url);

  if (url.pathname === '/') {
    navigateEvent.intercept({handler: loadIndexPage});
  } else if (url.pathname === '/cats/') {
    navigateEvent.intercept({handler: loadCatsPage});
  }
});

Bạn có thể xử lý thao tác điều hướng theo một trong hai cách sau:

  • Gọi intercept({ handler }) (như mô tả ở trên) để xử lý thao tác điều hướng.
  • Gọi preventDefault(), thao tác này có thể huỷ hoàn toàn hoạt động điều hướng.

Ví dụ này gọi intercept() trên sự kiện. Trình duyệt gọi lệnh gọi lại handler của bạn. Lệnh gọi lại này sẽ định cấu hình trạng thái tiếp theo của trang web. Thao tác này sẽ tạo một đối tượng chuyển đổi, navigation.transition, mà mã khác có thể dùng để theo dõi tiến trình điều hướng.

Cả intercept()preventDefault() thường được phép gọi, nhưng có những trường hợp không gọi được. Bạn không thể xử lý các thao tác điều hướng thông qua intercept() nếu thao tác điều hướng là thao tác điều hướng trên nhiều nguồn. Bạn không thể huỷ thao tác điều hướng thông qua preventDefault() nếu người dùng đang nhấn nút Quay lại hoặc Chuyển tiếp trong trình duyệt của họ; bạn không được phép giữ chân người dùng trên trang web của mình. (Vấn đề này đang được thảo luận trên GitHub.)

Ngay cả khi bạn không thể dừng hoặc chặn chính thao tác điều hướng, sự kiện "navigate" vẫn sẽ kích hoạt. Đây là một thông tin, vì vậy, mã của bạn có thể ghi lại một sự kiện Analytics để cho biết rằng người dùng đang rời khỏi trang web của bạn.

Tại sao nên thêm một sự kiện khác vào nền tảng?

Trình nghe sự kiện "navigate" tập trung xử lý các thay đổi về URL trong một SPA. Đây là một đề xuất khó thực hiện khi dùng các API cũ. Nếu đã từng viết quy trình định tuyến cho SPA của riêng mình bằng History API, thì có thể bạn đã thêm mã như sau:

function updatePage(event) {
  event.preventDefault(); // we're handling this link
  window.history.pushState(null, '', event.target.href);
  // TODO: set up page based on new URL
}
const links = [...document.querySelectorAll('a[href]')];
links.forEach(link => link.addEventListener('click', updatePage));

Điều này là ổn, nhưng chưa đầy đủ. Các đường liên kết có thể xuất hiện và biến mất trên trang của bạn, đồng thời đây không phải là cách duy nhất để người dùng di chuyển qua các trang. Ví dụ: họ có thể gửi biểu mẫu hoặc thậm chí sử dụng bản đồ hình ảnh. Trang của bạn có thể xử lý những trường hợp này, nhưng có rất nhiều trường hợp có thể được đơn giản hoá – đó là điều mà Navigation API mới đạt được.

Ngoài ra, những thay đổi trên không xử lý thao tác điều hướng tiến/lùi. Có một sự kiện khác cho việc đó, "popstate".

Theo tôi, History API thường có vẻ như có thể giúp ích phần nào cho những trường hợp này. Tuy nhiên, nó thực sự chỉ có 2 vùng bề mặt: phản hồi nếu người dùng nhấn nút Quay lại hoặc Chuyển tiếp trong trình duyệt, cộng với việc đẩy và thay thế URL. Nó không có điểm tương đồng với "navigate", trừ phi bạn thiết lập trình nghe cho các sự kiện nhấp một cách thủ công, chẳng hạn như minh hoạ ở trên.

Quyết định cách xử lý một thao tác điều hướng

navigateEvent chứa nhiều thông tin về hoạt động chỉ đường mà bạn có thể dùng để quyết định cách xử lý một hoạt động chỉ đường cụ thể.

Sau đây là các thuộc tính chính:

canIntercept
Nếu giá trị này là false, bạn sẽ không thể chặn thao tác điều hướng. Bạn không thể chặn các thao tác điều hướng trên nhiều nguồn gốc và các thao tác duyệt qua nhiều tài liệu.
destination.url
Có lẽ đây là thông tin quan trọng nhất cần cân nhắc khi xử lý thao tác điều hướng.
hashChange
True nếu thao tác điều hướng là cùng tài liệu và hàm băm là phần duy nhất của URL khác với URL hiện tại. Trong các SPA hiện đại, hàm băm phải dùng để liên kết đến các phần khác nhau của tài liệu hiện tại. Vì vậy, nếu hashChange là true, có thể bạn không cần chặn thao tác điều hướng này.
downloadRequest
Nếu đúng, thì hoạt động điều hướng được bắt đầu bằng một đường liên kết có thuộc tính download. Trong hầu hết các trường hợp, bạn không cần chặn sự kiện này.
formData
Nếu giá trị này không phải là giá trị rỗng, thì thao tác điều hướng này là một phần của việc gửi biểu mẫu POST. Hãy nhớ tính đến điều này khi xử lý thao tác điều hướng. Nếu bạn chỉ muốn xử lý các thao tác điều hướng GET, hãy tránh chặn các thao tác điều hướng mà formData không phải là giá trị rỗng. Hãy xem ví dụ về cách xử lý lượt gửi biểu mẫu ở phần sau của bài viết.
navigationType
Đây là một trong các giá trị "reload", "push", "replace" hoặc "traverse". Nếu là "traverse", thì bạn không thể huỷ chỉ đường qua preventDefault().

Ví dụ: hàm shouldNotIntercept được dùng trong ví dụ đầu tiên có thể trông như sau:

function shouldNotIntercept(navigationEvent) {
  return (
    !navigationEvent.canIntercept ||
    // If this is just a hashChange,
    // just let the browser handle scrolling to the content.
    navigationEvent.hashChange ||
    // If this is a download,
    // let the browser perform the download.
    navigationEvent.downloadRequest ||
    // If this is a form submission,
    // let that go to the server.
    navigationEvent.formData
  );
}

Chặn

Khi mã của bạn gọi intercept({ handler }) từ trong trình nghe "navigate", mã này sẽ thông báo cho trình duyệt rằng mã đang chuẩn bị trang cho trạng thái mới, đã cập nhật và quá trình điều hướng có thể mất một chút thời gian.

Trình duyệt bắt đầu bằng cách ghi lại vị trí cuộn cho trạng thái hiện tại, vì vậy, trình duyệt có thể khôi phục vị trí này sau đó (nếu muốn), sau đó trình duyệt sẽ gọi lệnh gọi lại handler của bạn. Nếu handler trả về một promise (điều này sẽ tự động xảy ra với các hàm không đồng bộ), thì promise đó sẽ cho trình duyệt biết thời gian điều hướng và liệu quá trình điều hướng có thành công hay không.

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

Do đó, API này giới thiệu một khái niệm ngữ nghĩa mà trình duyệt hiểu được: quá trình điều hướng SPA hiện đang diễn ra, theo thời gian, thay đổi tài liệu từ một URL và trạng thái trước đó thành một URL và trạng thái mới. Điều này mang lại một số lợi ích tiềm năng, bao gồm cả khả năng hỗ trợ tiếp cận: trình duyệt có thể hiển thị điểm bắt đầu, điểm kết thúc hoặc khả năng xảy ra lỗi của một thao tác điều hướng. Ví dụ: Chrome kích hoạt chỉ báo tải gốc và cho phép người dùng tương tác với nút dừng. (Hiện tại, điều này không xảy ra khi người dùng di chuyển bằng nút quay lại/tiến, nhưng vấn đề này sẽ sớm được khắc phục.)

Khi chặn các thao tác điều hướng, URL mới sẽ có hiệu lực ngay trước khi lệnh gọi lại handler của bạn được gọi. Nếu bạn không cập nhật DOM ngay lập tức, thì sẽ có một khoảng thời gian mà nội dung cũ hiển thị cùng với URL mới. Điều này ảnh hưởng đến những việc như giải quyết URL tương đối khi tìm nạp dữ liệu hoặc tải các tài nguyên phụ mới.

Một cách để trì hoãn việc thay đổi URL đang được thảo luận trên GitHub, nhưng bạn nên cập nhật ngay trang bằng một loại phần giữ chỗ nào đó cho nội dung sắp tới:

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
      },
    });
  }
});

Điều này không chỉ giúp bạn tránh được các vấn đề về phân giải URL mà còn mang lại cảm giác nhanh chóng vì bạn đang phản hồi ngay lập tức cho người dùng.

Tín hiệu huỷ

Vì bạn có thể thực hiện công việc không đồng bộ trong trình xử lý intercept(), nên có thể thao tác điều hướng sẽ trở nên dư thừa. Điều này xảy ra khi:

  • Người dùng nhấp vào một đường liên kết khác hoặc một số mã thực hiện một thao tác điều hướng khác. Trong trường hợp này, thanh điều hướng cũ sẽ bị loại bỏ để chuyển sang thanh điều hướng mới.
  • Người dùng nhấp vào nút "dừng" trong trình duyệt.

Để xử lý bất kỳ trường hợp nào trong số này, sự kiện được truyền đến trình nghe "navigate" sẽ chứa một thuộc tính signal, đây là một AbortSignal. Để biết thêm thông tin, hãy xem phần Tìm nạp có thể huỷ.

Nói tóm lại, về cơ bản, nó cung cấp một đối tượng kích hoạt sự kiện khi bạn nên dừng công việc. Cụ thể, bạn có thể truyền một AbortSignal đến mọi lệnh gọi mà bạn thực hiện đến fetch(). Lệnh gọi này sẽ huỷ các yêu cầu mạng đang diễn ra nếu quá trình điều hướng bị chặn. Điều này sẽ giúp tiết kiệm băng thông của người dùng và từ chối Promise do fetch() trả về, ngăn mọi mã sau đó thực hiện các thao tác như cập nhật DOM để hiện một thao tác điều hướng trang hiện không hợp lệ.

Sau đây là ví dụ trước đó, nhưng có getArticleContent nội tuyến, cho thấy cách dùng AbortSignal với fetch():

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        // The URL has already changed, so quickly show a placeholder.
        renderArticlePagePlaceholder();
        // Then fetch the real data.
        const articleContentURL = new URL(
          '/get-article-content',
          location.href
        );
        articleContentURL.searchParams.set('path', url.pathname);
        const response = await fetch(articleContentURL, {
          signal: navigateEvent.signal,
        });
        const articleContent = await response.json();
        renderArticlePage(articleContent);
      },
    });
  }
});

Xử lý thao tác cuộn

Khi bạn intercept() một thành phần điều hướng, trình duyệt sẽ cố gắng tự động xử lý thao tác cuộn.

Đối với các thao tác điều hướng đến một mục nhập mới trong nhật ký (khi navigationEvent.navigationType"push" hoặc "replace"), điều này có nghĩa là cố gắng cuộn đến phần được chỉ định bằng đoạn URL (phần sau dấu #) hoặc đặt lại thao tác cuộn về đầu trang.

Đối với các lần tải lại và di chuyển, điều này có nghĩa là khôi phục vị trí cuộn về vị trí của lần hiển thị gần đây nhất của mục nhập này trong nhật ký.

Theo mặc định, điều này xảy ra khi lời hứa do handler của bạn trả về được giải quyết, nhưng nếu có ý nghĩa khi cuộn sớm hơn, bạn có thể gọi navigateEvent.scroll():

navigation.addEventListener('navigate', navigateEvent => {
  if (shouldNotIntercept(navigateEvent)) return;
  const url = new URL(navigateEvent.destination.url);

  if (url.pathname.startsWith('/articles/')) {
    navigateEvent.intercept({
      async handler() {
        const articleContent = await getArticleContent(url.pathname);
        renderArticlePage(articleContent);
        navigateEvent.scroll();

        const secondaryContent = await getSecondaryContent(url.pathname);
        addSecondaryContent(secondaryContent);
      },
    });
  }
});

Ngoài ra, bạn có thể chọn không sử dụng tính năng xử lý thao tác cuộn tự động bằng cách đặt tuỳ chọn scroll của intercept() thành "manual":

navigateEvent.intercept({
  scroll: 'manual',
  async handler() {
    // …
  },
});

Xử lý tiêu điểm

Sau khi promise do handler của bạn trả về được giải quyết, trình duyệt sẽ tập trung vào phần tử đầu tiên có thuộc tính autofocus được đặt hoặc phần tử <body> nếu không có phần tử nào có thuộc tính đó.

Bạn có thể chọn không áp dụng hành vi này bằng cách đặt lựa chọn focusReset của intercept() thành "manual":

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

Sự kiện thành công và không thành công

Khi trình xử lý intercept() của bạn được gọi, một trong hai điều sau sẽ xảy ra:

  • Nếu Promise được trả về đáp ứng (hoặc bạn không gọi intercept()), thì Navigation API sẽ kích hoạt "navigatesuccess" bằng Event.
  • Nếu Promise được trả về từ chối, API sẽ kích hoạt "navigateerror" bằng một ErrorEvent.

Các sự kiện này cho phép mã của bạn xử lý thành công hoặc thất bại theo cách tập trung. Ví dụ: bạn có thể xử lý thành công bằng cách ẩn chỉ báo tiến trình đã hiển thị trước đó, như sau:

navigation.addEventListener('navigatesuccess', event => {
  loadingIndicator.hidden = true;
});

Hoặc bạn có thể hiện thông báo lỗi khi thất bại:

navigation.addEventListener('navigateerror', event => {
  loadingIndicator.hidden = true; // also hide indicator
  showMessage(`Failed to load page: ${event.message}`);
});

Trình nghe sự kiện "navigateerror" (nhận ErrorEvent) đặc biệt hữu ích vì trình nghe này đảm bảo nhận được mọi lỗi từ mã của bạn khi thiết lập một trang mới. Bạn có thể chỉ cần await fetch() khi biết rằng nếu mạng không hoạt động, lỗi sẽ được chuyển đến "navigateerror".

navigation.currentEntry cung cấp quyền truy cập vào mục nhập hiện tại. Đây là một đối tượng mô tả vị trí hiện tại của người dùng. Mục này bao gồm URL hiện tại, siêu dữ liệu có thể dùng để xác định mục này theo thời gian và trạng thái do nhà phát triển cung cấp.

Siêu dữ liệu bao gồm key, một thuộc tính chuỗi duy nhất của mỗi mục, đại diện cho mục hiện tại và vị trí của mục đó. Khoá này vẫn giữ nguyên ngay cả khi URL hoặc trạng thái của mục nhập hiện tại thay đổi. Vẫn ở vị trí cũ. Ngược lại, nếu người dùng nhấn nút Quay lại rồi mở lại cùng một trang, thì key sẽ thay đổi vì mục nhập mới này tạo ra một vị trí mới.

Đối với nhà phát triển, key rất hữu ích vì Navigation API cho phép bạn điều hướng trực tiếp người dùng đến một mục có khoá khớp. Bạn có thể giữ lại trạng thái này, ngay cả trong trạng thái của các mục nhập khác, để dễ dàng chuyển đổi giữa các trang.

// On JS startup, get the key of the first loaded page
// so the user can always go back there.
const {key} = navigation.currentEntry;
backToHomeButton.onclick = () => navigation.traverseTo(key);

// Navigate away, but the button will always work.
await navigation.navigate('/another_url').finished;

Tiểu bang

Navigation API (API Điều hướng) hiển thị khái niệm về "trạng thái", là thông tin do nhà phát triển cung cấp, được lưu trữ liên tục trong mục nhập nhật ký hiện tại nhưng người dùng không nhìn thấy trực tiếp. API này cực kỳ giống với history.state trong History API, nhưng đã được cải thiện.

Trong Navigation API, bạn có thể gọi phương thức .getState() của mục nhập hiện tại (hoặc bất kỳ mục nhập nào) để trả về bản sao trạng thái của mục nhập đó:

console.log(navigation.currentEntry.getState());

Theo mặc định, giá trị này sẽ là undefined.

Trạng thái của chế độ cài đặt

Mặc dù các đối tượng trạng thái có thể bị thay đổi, nhưng những thay đổi đó sẽ không được lưu lại cùng với mục nhập nhật ký, vì vậy:

const state = navigation.currentEntry.getState();
console.log(state.count); // 1
state.count++;
console.log(state.count); // 2
// But:
console.info(navigation.currentEntry.getState().count); // will still be 1

Cách đặt trạng thái chính xác là trong quá trình điều hướng tập lệnh:

navigation.navigate(url, {state: newState});
// Or:
navigation.reload({state: newState});

Trong đó newState có thể là bất kỳ đối tượng có thể sao chép nào.

Nếu muốn cập nhật trạng thái của mục hiện tại, tốt nhất là bạn nên thực hiện một thao tác điều hướng để thay thế mục hiện tại:

navigation.navigate(location.href, {state: newState, history: 'replace'});

Sau đó, trình nghe sự kiện "navigate" có thể nhận được thay đổi này thông qua navigateEvent.destination:

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

Cập nhật trạng thái đồng bộ

Nhìn chung, bạn nên cập nhật trạng thái không đồng bộ thông qua navigation.reload({state: newState}), sau đó trình nghe "navigate" có thể áp dụng trạng thái đó. Tuy nhiên, đôi khi, thay đổi trạng thái đã được áp dụng hoàn toàn vào thời điểm mã của bạn nhận được thông tin về thay đổi đó, chẳng hạn như khi người dùng bật/tắt một phần tử <details> hoặc người dùng thay đổi trạng thái của một thành phần đầu vào biểu mẫu. Trong những trường hợp này, bạn có thể muốn cập nhật trạng thái để những thay đổi này được giữ nguyên trong quá trình tải lại và duyệt qua. Bạn có thể thực hiện việc này bằng cách dùng updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

Ngoài ra, bạn có thể tham gia một sự kiện để tìm hiểu về thay đổi này:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

Tuy nhiên, nếu thấy mình phản ứng với các thay đổi về trạng thái trong "currententrychange", bạn có thể đang phân tách hoặc thậm chí sao chép mã xử lý trạng thái giữa sự kiện "navigate" và sự kiện "currententrychange", trong khi navigation.reload({state: newState}) sẽ cho phép bạn xử lý mã này ở một nơi.

Trạng thái so với tham số URL

Vì trạng thái có thể là một đối tượng có cấu trúc, nên bạn có thể muốn dùng đối tượng này cho tất cả trạng thái ứng dụng. Tuy nhiên, trong nhiều trường hợp, bạn nên lưu trạng thái đó trong URL.

Nếu bạn muốn trạng thái được giữ lại khi người dùng chia sẻ URL với một người dùng khác, hãy lưu trạng thái đó trong URL. Nếu không, đối tượng trạng thái sẽ là lựa chọn tốt hơn.

Truy cập vào tất cả các mục

Tuy nhiên, "mục nhập hiện tại" không phải là tất cả. API này cũng cung cấp một cách để truy cập vào toàn bộ danh sách các mục mà người dùng đã truy cập trong khi sử dụng trang web của bạn thông qua lệnh gọi navigation.entries(). Lệnh gọi này sẽ trả về một mảng ảnh chụp nhanh các mục. Bạn có thể dùng thông tin này để, ví dụ: cho thấy một giao diện người dùng khác dựa trên cách người dùng chuyển đến một trang nhất định, hoặc chỉ để xem lại các URL trước đó hoặc trạng thái của chúng. Điều này là không thể với History API hiện tại.

Bạn cũng có thể theo dõi sự kiện "dispose" trên từng NavigationHistoryEntry, sự kiện này sẽ kích hoạt khi mục không còn nằm trong nhật ký trình duyệt nữa. Điều này có thể xảy ra trong quá trình dọn dẹp chung, nhưng cũng có thể xảy ra khi điều hướng. Ví dụ: nếu bạn quay lại 10 vị trí, sau đó chuyển tiếp, thì 10 mục trong nhật ký đó sẽ bị loại bỏ.

Ví dụ

Sự kiện "navigate" sẽ kích hoạt cho tất cả các loại thao tác điều hướng, như đã đề cập ở trên. (Thật ra, có một phụ lục dài trong quy cách về tất cả các loại có thể có.)

Mặc dù đối với nhiều trang web, trường hợp phổ biến nhất là khi người dùng nhấp vào <a href="...">, nhưng có 2 loại điều hướng phức tạp hơn đáng chú ý mà bạn nên xem xét.

Điều hướng có lập trình

Đầu tiên là điều hướng có lập trình, trong đó hoạt động điều hướng là do một lệnh gọi phương thức bên trong mã phía máy khách của bạn gây ra.

Bạn có thể gọi navigation.navigate('/another_page') từ bất kỳ vị trí nào trong mã để gây ra một thao tác điều hướng. Việc này sẽ do trình nghe sự kiện tập trung đã đăng ký trên trình nghe "navigate" xử lý và trình nghe tập trung của bạn sẽ được gọi đồng bộ.

Đây là một phương pháp tổng hợp được cải tiến của các phương thức cũ hơn như location.assign() và các phương thức khác, cộng với các phương thức pushState()replaceState() của History API.

Phương thức navigation.navigate() trả về một đối tượng chứa 2 thực thể Promise trong { committed, finished }. Điều này cho phép phương thức gọi có thể đợi cho đến khi quá trình chuyển đổi được "cam kết" (URL hiển thị đã thay đổi và có một NavigationHistoryEntry mới) hoặc "kết thúc" (tất cả các promise do intercept({ handler }) trả về đều hoàn tất hoặc bị từ chối do lỗi hoặc bị một thao tác điều hướng khác chiếm quyền).

Phương thức navigate cũng có một đối tượng tuỳ chọn, trong đó bạn có thể đặt:

  • state: trạng thái cho mục nhập mới trong nhật ký, có sẵn thông qua phương thức .getState() trên NavigationHistoryEntry.
  • history: có thể đặt thành "replace" để thay thế mục nhập hiện tại trong nhật ký.
  • info: một đối tượng để truyền đến sự kiện điều hướng thông qua navigateEvent.info.

Cụ thể, info có thể hữu ích, chẳng hạn như để biểu thị một ảnh động cụ thể khiến trang tiếp theo xuất hiện. (Một cách khác là đặt một biến chung hoặc đưa biến đó vào một phần của #hash. Cả hai lựa chọn đều hơi khó xử.) Đặc biệt, info này sẽ không được phát lại nếu sau đó người dùng thực hiện thao tác điều hướng, chẳng hạn như thông qua nút Quay lại và Chuyển tiếp. Trên thực tế, giá trị này sẽ luôn là undefined trong những trường hợp đó.

Bản minh hoạ thao tác mở từ trái hoặc phải

navigation cũng có một số phương thức điều hướng khác, tất cả đều trả về một đối tượng chứa { committed, finished }. Tôi đã đề cập đến traverseTo() (chấp nhận một key biểu thị một mục cụ thể trong nhật ký của người dùng) và navigate(). Thẻ này cũng bao gồm back(), forward()reload(). Tất cả các phương thức này đều được xử lý (giống như navigate()) bằng trình nghe sự kiện "navigate" tập trung.

Lượt gửi biểu mẫu

Thứ hai, việc gửi <form> HTML qua POST là một loại thao tác điều hướng đặc biệt và Navigation API có thể chặn thao tác này. Mặc dù có thêm một trọng tải, nhưng hoạt động điều hướng vẫn được trình nghe "navigate" xử lý tập trung.

Bạn có thể phát hiện lượt gửi biểu mẫu bằng cách tìm thuộc tính formData trên NavigateEvent. Dưới đây là ví dụ đơn giản chuyển mọi lượt gửi biểu mẫu thành một lượt gửi ở lại trang hiện tại thông qua fetch():

navigation.addEventListener('navigate', navigateEvent => {
  if (navigateEvent.formData && navigateEvent.canIntercept) {
    // User submitted a POST form to a same-domain URL
    // (If canIntercept is false, the event is just informative:
    // you can't intercept this request, although you could
    // likely still call .preventDefault() to stop it completely).

    navigateEvent.intercept({
      // Since we don't update the DOM in this navigation,
      // don't allow focus or scrolling to reset:
      focusReset: 'manual',
      scroll: 'manual',
      handler() {
        await fetch(navigateEvent.destination.url, {
          method: 'POST',
          body: navigateEvent.formData,
        });
        // You could navigate again with {history: 'replace'} to change the URL here,
        // which might indicate "done"
      },
    });
  }
});

Thông tin nào còn thiếu?

Mặc dù trình nghe sự kiện "navigate" có tính chất tập trung, nhưng quy cách Navigation API hiện tại không kích hoạt "navigate" khi trang tải lần đầu. Đối với những trang web sử dụng Kết xuất phía máy chủ (SSR) cho tất cả các trạng thái, điều này có thể không sao – máy chủ của bạn có thể trả về trạng thái ban đầu chính xác, đây là cách nhanh nhất để cung cấp nội dung cho người dùng. Nhưng những trang web tận dụng mã phía máy khách để tạo trang có thể cần tạo thêm một hàm để khởi tạo trang.

Một lựa chọn thiết kế có chủ ý khác của Navigation API là chỉ hoạt động trong một khung hình duy nhất, tức là trang cấp cao nhất hoặc một <iframe> cụ thể duy nhất. Điều này có một số ý nghĩa thú vị được ghi lại thêm trong quy cách, nhưng trên thực tế, điều này sẽ giúp nhà phát triển bớt nhầm lẫn. History API trước đây có một số trường hợp đặc biệt gây nhầm lẫn, chẳng hạn như hỗ trợ khung hình và Navigation API được thiết kế lại sẽ xử lý những trường hợp đặc biệt này ngay từ đầu.

Cuối cùng, vẫn chưa có sự đồng thuận về việc sửa đổi hoặc sắp xếp lại danh sách các mục mà người dùng đã điều hướng thông qua một cách có lập trình. Vấn đề này hiện đang được thảo luận, nhưng một lựa chọn có thể là chỉ cho phép xoá: hoặc là các mục trong nhật ký hoặc "tất cả các mục trong tương lai". Cách thứ hai sẽ cho phép trạng thái tạm thời. Ví dụ: là nhà phát triển, tôi có thể:

  • hỏi người dùng một câu hỏi bằng cách chuyển đến URL hoặc trạng thái mới
  • cho phép người dùng hoàn tất công việc (hoặc quay lại)
  • xoá một mục trong nhật ký sau khi hoàn thành một việc cần làm

Điều này có thể phù hợp với các phương thức tạm thời hoặc quảng cáo xen kẽ: URL mới là thứ mà người dùng có thể sử dụng cử chỉ Quay lại để thoát, nhưng sau đó họ không thể vô tình chuyển tiếp để mở lại (vì mục nhập đã bị xoá). Điều này không thể thực hiện được với History API hiện tại.

Dùng thử Navigation API

Navigation API có trong Chrome 102 mà không cần cờ. Bạn cũng có thể thử bản minh hoạ của Domenic Denicola.

Mặc dù History API cổ điển có vẻ đơn giản, nhưng API này không được xác định rõ ràng và có rất nhiều vấn đề xung quanh các trường hợp đặc biệt và cách API này được triển khai khác nhau trên các trình duyệt. Chúng tôi hy vọng bạn sẽ cân nhắc việc gửi ý kiến phản hồi về Navigation API mới.

Tài liệu tham khảo

Lời cảm ơn

Cảm ơn Thomas Steiner, Domenic Denicola và Nate Chapin đã xem xét bài đăng này.