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

Tiêu chuẩn hoá việc định tuyến phía máy khách thông qua một API hoàn toàn mới, giúp cải tiến toàn bộ việc xây dựng ứng dụng trang đơn.

Hỗ trợ trình duyệt

  • Chrome: 102.
  • Edge: 102.
  • Firefox: không được hỗ trợ.
  • Safari: không được hỗ trợ.

Nguồn

Ứng dụng trang đơn (SPA) được xác định bằng một tính năng cốt lõi: tự động ghi lại nội dung 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 các trang hoàn toàn 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 API Nhật ký (hoặc trong một số ít 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 cồng kềnh được phát triển từ lâu trước khi SPA trở thành tiêu chuẩn — và web đang cần một phương pháp hoàn toàn mới. Navigation API là một API được đề xuất để hoàn toàn cải tiến không gian này, thay vì chỉ cố gắng vá các điểm yếu của History API. (Ví dụ: Cuộn khôi phục đã vá API Lịch sử thay vì cố gắng phát minh lại nó.)

Bài đăng này mô tả Navigation API ở cấp độ tổng quát. Để đọ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 trình nghe "navigate" trên đối tượng navigation toàn cục. Về cơ bản, sự kiện này là 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 một đườ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, thuộc tính 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 hành động đó. Đối với SPA, điều đó có thể 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.

NavigateEvent được truyền đến trình nghe "navigate" chứa thông tin về thao tác điều hướng, chẳng hạn như URL đích đến và cho phép bạn phản hồi thao tác điều hướng ở một nơi tập trung. Trình nghe "navigate" cơ bản có thể có dạ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ý điều hướng theo một trong hai cách:

  • 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 thao tác điều hướng.

Ví dụ này gọi intercept() trên sự kiện. Trình duyệt sẽ gọi lệnh gọi lại handler để đị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ể sử dụng để theo dõi tiến trình điều hướng.

Cả intercept()preventDefault() thường được cho phép, nhưng có một số trường hợp không thể 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 gốc. Ngoài ra, 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 Tiếp theo trong trình duyệt; bạn không được giữ 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 hoạt động điều hướng, sự kiện "navigate" vẫn sẽ kích hoạt. Đây là một sự kiện cung cấp 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 bên trong một SPA. Đây là một đề xuất khó khăn khi sử dụng các API cũ. Nếu bạn đã từng viết định tuyến cho SPA của riêng mình bằng cách sử dụng API Lịch sử, thì bạn có thể đã 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à bình thường, nhưng chưa toàn diện. Đườ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 có thể di chuyển qua các trang. Ví dụ: họ có thể gửi một 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 vấn đề này, nhưng có rất nhiều khả năng có thể được đơn giản hoá – điều mà Navigation API mới đạt được.

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

Theo cá nhân tôi, History API thường có vẻ như có thể giúp ích cho những khả năng này. Tuy nhiên, lớp này thực sự chỉ có hai khu vực nền tảng: phản hồi nếu người dùng nhấn Quay lại hoặc Tiếp tục trong trình duyệt, cộng với việc đẩy và thay thế các URL. API này không giống với "navigate", ngoại trừ trường hợp bạn thiết lập trình nghe theo cách thủ công cho các sự kiện nhấp chuột, ví dụ như minh hoạ ở trên.

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

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

Các thuộc tính chính là:

canIntercept
Nếu giá trị này là sai, bạn không thể chặn thao tác điều hướng. Không thể chặn các thao tác điều hướng và di chuyển giữa các tài liệu.
destination.url
Có lẽ là thông tin quan trọng nhất cần xem xét khi xử lý quá trình điều hướng.
hashChange
Đúng nếu điều hướng là cùng một 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, bạn nên băm dữ liệu để liên kết đến nhiều phần 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 như vậy, thì quá trình điều hướng sẽ 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 việc này.
formData
Nếu 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 quá trình gửi biểu mẫu POST. Hãy nhớ tính đến điều này khi xử lý quá trình đ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ỷ thao tác điều hướng này thông qua preventDefault().

Ví dụ: hàm shouldNotIntercept dùng trong ví dụ đầu tiên có thể có dạ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ã gọi intercept({ handler }) từ trình nghe "navigate", mã 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à việc điều hướng có thể mất chút thời gian.

Trình duyệt bắt đầu bằng cách chụp vị trí cuộn cho trạng thái hiện tại, vì vậy, bạn có thể khôi phục vị trí này sau (không bắt buộc), sau đó gọi lệnh gọi lại handler. Nếu handler trả về một lời hứa (xảy ra tự động với các hàm không đồng bộ), thì lời hứa đó sẽ cho trình duyệt biết thời gian di chuyển và liệu quá trình di chuyển 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: một thao tác điều hướng SPA đang diễn ra, theo thời gian, thay đổi tài liệu 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 có 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ị phần bắt đầu, kết thúc hoặc lỗi tiềm ẩn 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. (Điều này hiện không xảy ra khi người dùng điều hướng qua các nút tiến/lùi, nhưng điều 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 gọi. Nếu bạn không cập nhật DOM ngay lập tức, việc này sẽ tạo ra một khoảng thời gian trong đó nội dung cũ được hiển thị cùng với URL mới. Điều này ảnh hưởng đến các vấn đề như độ phân giải URL tương đối khi tìm nạp dữ liệu hoặc tải tài nguyên phụ mới.

Chúng tôi đang thảo luận về cách trì hoãn việc thay đổi URL trên GitHub, nhưng bạn nên cập nhật ngay trang bằng một số phần giữ chỗ 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 tránh được các vấn đề về phân giải URL mà còn cho tốc độ nhanh vì bạn 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 thành phần điều hướng có thể trở thành không cần thiết. Đ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 thao tác điều hướng khác. Trong trường hợp này, thành phần điều hướng cũ sẽ bị bỏ qua để chuyển sang thành phần đ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ý mọi khả năng này, sự kiện được truyền đến trình nghe "navigate" chứa một thuộc tính signal, là một AbortSignal. Để biết thêm thông tin, hãy xem phần Tìm nạp có thể huỷ.

Phiên bản ngắn gọn về cơ bản là 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ủa mình. Đáng chú ý là bạn có thể truyền AbortSignal đến mọi lệnh gọi mà bạn thực hiện đến fetch(). Thao tác này sẽ huỷ các yêu cầu mạng đang bay nếu quá trình điều hướng bị giành quyền. Điều này vừa giúp tiết kiệm băng thông của người dùng, vừa 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 thị thao tác điều hướng trang hiện không hợp lệ.

Sau đây là ví dụ trước, nhưng với getArticleContent cùng dòng, cho thấy cách sử 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 thao tác điều hướng đến một mục nhật ký mới (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 phân đoạn URL (bit sau #) hoặc đặt lại thao tác cuộn về đầu trang.

Đối với tải lại và truyền tải, điều này có nghĩa là khôi phục vị trí cuộn về vị trí mà mục nhập lịch sử này hiển thị lần gần đây nhất.

Theo mặc định, việc này xảy ra sau khi lời hứa do handler trả về được giải quyết, nhưng nếu cần 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 hoàn toàn không sử dụng tính năng xử lý 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 lời hứa do handler trả về được giải quyết, trình duyệt sẽ đặt tiêu điểm vào phần tử đầu tiên có thuộc tính autofocus 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 tuỳ 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 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 không thành công 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ị thông báo lỗi khi không thành công:

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ì đảm bảo nhận được mọi lỗi từ mã đang thiết lập trang mới. Bạn chỉ cần await fetch() biết rằng nếu không có mạng, lỗi cuối cùng 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à đố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 nhập đại diện cho mục nhập hiện tại và khe của mục nhập đó. Khoá này vẫn giữ nguyên ngay cả khi URL hoặc trạng thái của mục hiện tại thay đổi. Thẻ vẫn ở cùng một khe. Ngược lại, nếu người dùng nhấn vào Quay lại rồi mở lại cùng một trang, thì key sẽ thay đổi khi mục nhập mới này tạo một khung giờ mới.

Đối với nhà phát triển, key rất hữu ích vì API điều hướng cho phép bạn chuyển trực tiếp người dùng đến một mục bằng khoá trùng khớp. Bạn có thể giữ lại trạng thái này, ngay cả ở 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 hiển thị khái niệm "trạng thái". Đây là thông tin do nhà phát triển cung cấp, được lưu trữ liên tục trên mục nhật ký hiện tại nhưng người dùng không thể trực tiếp nhìn thấy. API này rất giống với history.state trong API Nhật ký, nhưng được cải tiến.

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

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

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

Thiết lập trạng thái

Mặc dù các đối tượng trạng thái có thể thay đổi được, nhưng những thay đổi đó sẽ không được lưu ngược lại bằng 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 chính xác để đặt trạng thái là trong khi điều hướng tập lệnh:

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

Trong đó, newState có thể là mọi đối tượng có thể sao chép.

Nếu bạn muốn cập nhật trạng thái của mục hiện tại, tốt nhất bạn nên thực hiện 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 ra 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 đầy đủ 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 dữ liệu đầu vào trong biểu mẫu. Trong những trường hợp này, bạn nên cập nhật trạng thái để các thay đổi này được giữ nguyên thông qua các lần tải lại và truy cập. Bạn có thể thực hiện việc này bằng cách sử dụng updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

Chúng tôi cũng sẽ tổ chức một sự kiện để bạ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", thì có thể bạn đang 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}) cho phép bạn xử lý mã ở một nơi.

Tham số 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 muốn sử dụng trạng thái này cho tất cả trạng thái của ứng dụng. Tuy nhiên, trong nhiều trường hợp, bạn nên lưu trữ trạng thái đó trong URL.

Nếu bạn muốn trạng thái được giữ nguyên khi người dùng chia sẻ URL với người dùng khác, hãy lưu trữ 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 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 tổng quan nhanh về các mục. Bạn có thể dùng thông tin này để hiển thị 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 các URL đó. Điều này không thể thực hiện được với API Nhật ký hiện tại.

Bạn cũng có thể theo dõi sự kiện "dispose" trên từng NavigationHistoryEntry riêng lẻ. Sự kiện này sẽ được kích hoạt khi mục nhập không còn nằm trong nhật ký duyệt web 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 di chuyển ngược lại 10 địa điểm rồi di chuyển về phía trước, thì 10 mục nhập nhật ký đó sẽ bị loại bỏ.

Ví dụ

Sự kiện "navigate" sẽ kích hoạt cho mọi loại thao tác điều hướng, như đã đề cập ở trên. (Thực ra, có một phần phụ lục dài trong thông số kỹ thuật 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ó hai loại điều hướng phức tạp hơn đáng chú ý.

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

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

Bạn có thể gọi navigation.navigate('/another_page') từ bất kỳ vị trí nào trong mã để đ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 thức tổng hợp cải tiến của các phương thức cũ 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 API Nhật ký.

Phương thức navigation.navigate() trả về một đối tượng chứa hai 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 thực hiện" (URL hiển thị đã thay đổi và có NavigationHistoryEntry mới) hoặc "hoàn tất" (tất cả các lời hứa do intercept({ handler }) trả về đều hoàn tất hoặc bị từ chối do không thành công 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 của mục nhập nhật ký mới, có sẵn thông qua phương thức .getState() trên NavigationHistoryEntry.
  • history: có thể được đặt thành "replace" để thay thế mục nhập nhật ký hiện tại.
  • 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 để, ví dụ: biểu thị một ảnh động cụ thể khiến trang tiếp theo xuất hiện. (Cách thay thế có thể là đặt một biến toàn cục hoặc đưa biến đó vào #hash. Cả hai lựa chọn đều hơi khó xử.) Đáng chú ý là 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, ví dụ: thông qua các nút Quay lại và Tiếp tục. Trên thực tế, giá trị này sẽ luôn là undefined trong các trường hợp đó.

Bản minh hoạ cách mở từ bên trái hoặc bên 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 key biểu thị một mục cụ thể trong nhật ký của người dùng) và navigate(). Tệp 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.

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 đó. Mặc dù bao gồm một tải trọng bổ sung, nhưng hoạt động điều hướng vẫn do trình nghe "navigate" xử lý tập trung.

Bạn có thể phát hiện việc gửi biểu mẫu bằng cách tìm thuộc tính formData trên NavigateEvent. Dưới đây là một ví dụ chỉ cần chuyển mọi lượt gửi biểu mẫu thành một biểu mẫu ở lại trên 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ó bản chất tập trung, nhưng thông số kỹ thuật Navigation API hiện tại không kích hoạt "navigate" trong lần tải đầu tiên của trang. Đối với những trang web sử dụng tính năng Kết xuất phía máy chủ (SSR) cho tất cả các trạng thái, thì điều này có thể không gây ra vấn đề gì – 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. Tuy nhiên, các trang web tận dụng mã phía máy khách để tạo trang có thể cần tạo một hàm bổ sung để khởi chạy trang.

Một lựa chọn thiết kế có chủ ý khác của Navigation API là API này chỉ hoạt động trong một khung duy nhất, tức là trang cấp cao nhất hoặc một <iframe> cụ thể. Điều này có một số tác động thú vị được ghi nhận thêm trong thông số kỹ thuật, nhưng trong thực tế, sẽ giúp nhà phát triển ít nhầm lẫn hơn. API Nhật ký 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à API Điều hướng được thiết kế lại xử lý những trường hợp đặc biệt này ngay từ đầu.

Cuối cùng, 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 bằng cách lập trình. Vấn đề này đang được thảo luận, nhưng một lựa chọn có thể là chỉ cho phép xoá: các mục nhập trước đây hoặc "tất cả mục nhập trong tương lai". Chính sách sau sẽ cho phép sử dụng trạng thái tạm thời. Ví dụ: với tư cách 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 thành công việc của họ (hoặc Quay lại)
  • xóa mục nhập lịch sử khi hoàn thành một nhiệm vụ

Đ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à URL 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 đi tiếp để mở lại URL (vì mục nhập đã bị xóa). Bạn không thể làm việc này với API Nhật ký hiện tại.

Dùng thử Navigation API

API Điều hướng có trong Chrome 102 mà không cần cờ. Bạn cũng có thể thử một bản minh hoạ của Domenic Denicola.

Mặc dù API Nhật ký 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 đề liên quan đến các trường hợp hiếm gặp 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 cân nhắc việc đưa ra ý 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.