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

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

Hỗ trợ trình duyệt

  • 102
  • 102
  • x
  • x

Nguồn

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

Mặc dù các SPA có thể cung cấp cho bạn tính năng này thông qua History API (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 đó là một API bất tiện được phát triển từ lâu trước khi SPA trở thành tiêu chuẩn – và web đang kêu ca về một phương pháp hoàn toàn mới. Navigation API là một API được đề xuất để cải tiến toàn bộ không gian này thay vì chỉ cố gắng vá các cạnh thô của API Lịch sử. (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ả tổng quan về Navigation API (API Điều hướng). Nếu bạn muốn đọc đề xuất về kỹ thuật, hãy xem Báo cáo dự thảo trong kho lưu trữ của WICG.

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

Để sử dụng API Điều hướng, hãy bắt đầu bằng cách thêm trình nghe "navigate" trên đối tượng navigation chung. Sự kiện này về cơ bản là tập trung: nó sẽ kích hoạt cho tất cả các loại thao tác, 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à tiến lên) hay khi hoạt động đ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, nó cho phép mã của bạn ghi đè hành vi mặc định của trình duyệt cho 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ề hoạt động điều hướng (chẳng hạn như URL đích) và cho phép bạn phản hồi hoạt động đ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ý hoạt động điều hướng.
  • Đang gọi preventDefault(). Thao tác này có thể huỷ hoàn toàn quá trình đ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. 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 cho phép, nhưng có một số trường hợp không gọi được. Bạn không thể xử lý hoạt động điều hướng thông qua intercept() nếu điều hướng là đ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 qua preventDefault() nếu người dùng nhấn nút Tiến hoặc lùi trong trình duyệt; bạn sẽ không thể 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 hoạt động điều hướng, sự kiện "navigate" vẫn sẽ kích hoạt. Thẻ này cung cấp thông tin. Ví dụ: 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 tuyên bố 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 đầy đủ. Đường liên kết có thể xuất hiện và xuất hiện trên trang của bạn, đây không phải là cách duy nhất để người dùng điều hướng 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ể giải quyết các vấn đề này, nhưng có rất nhiều khả năng mà bạn có thể đơn giản hoá – một điều mà API Điều hướng mới đạt được.

Ngoài ra, mục 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".

Về mặt cá nhân, History API thường cảm thấy rằng nó có thể giúp giải quyết 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ý hoạt động điều hướng

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

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

canIntercept
Nếu giá trị này là false, bạn sẽ không thể chặn hoạt động điều hướng. Hoạt động điều hướng trên nhiều nguồn gốc và truyền tải trên nhiều tài liệu không thể bị chặn.
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 phần điều hướng sử dụng 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à đúng, thì có lẽ 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 giá trị này không rỗng, thì thao tác điều hướng này là một phần trong 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 trong đó formData không 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 này.
navigationType
Đây là một trong số "reload", "push", "replace" hoặc "traverse". Nếu giá trị là "traverse", thì bạn không thể huỷ thao tác này qua preventDefault().

Ví dụ: hàm shouldNotIntercept được sử 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
  );
}

Cắt bóng

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 đ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 để bạn có thể khôi phục sau này nếu muốn, sau đó gọi lệnh gọi lại handler. Nếu handler của bạn trả về một lời hứa (tự động diễn ra với async functions), thì lời hứa đó sẽ cho trình duyệt biết quá trình điều hướng mất bao lâu và liệu 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 đưa ra một khái niệm ngữ nghĩa mà trình duyệt hiểu được: một hoạt động đ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 đó sang một URL và trạng thái mới. Điều này có thể mang lại một số lợi ích, bao gồm cả khả năng hỗ trợ tiếp cận: các trình duyệt có thể cho thấy phần đầu, phần cuối hoặc khả năng điều hướng không thành công. Chẳng hạn như Chrome sẽ 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 những yếu tố 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.

Để trì hoãn việc thay đổi URL, chúng tôi sẽ thảo luận trên GitHub, nhưng bạn nên cập nhật trang ngay lập tức bằng một số loại 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 giúp bạn nhanh chóng phản hồi vì bạn phản hồi ngay lập tức cho người dùng.

Huỷ các tín hiệu

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 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ị bỏ và thay vào đó là đ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ỳ khả năng nào trong số này, sự kiện được truyền đến trình nghe "navigate" chứa một thuộc tính signal, đó là AbortSignal. Để biết thêm thông tin, hãy xem bài viết Tìm nạp có thể huỷ bỏ.

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 tới 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. Thao tác này sẽ vừa 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 đây thực hiện các hành động như cập nhật DOM để hiển thị thành phần đ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 mục điều hướng, trình duyệt sẽ tìm cách xử lý thao tác tự động cuộn.

Đối với các thao tác điều hướng đến một mục mới trong nhật ký (khi navigationEvent.navigationType"push" hoặc "replace"), tức là cố gắng cuộn đến phần được phân đoạn URL chỉ định (một bit sau #) hoặc đặt lại thao tác cuộn lên đầu trang.

Đối với lần 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, điều này xảy ra sau khi lời hứa mà handler trả về được phân giải, nhưng nếu bạn nê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 xác định được lời hứa mà handler trả về, trình duyệt sẽ lấy tiêu điểm vào phần tử đầu tiên với tập hợp thuộc tính autofocus hoặc lấy 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 sử 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 một 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 một 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ực hiện được thao tác:

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 được ErrorEvent đặc biệt hữu ích vì nó đảm bảo sẽ nhận được mọi lỗi từ mã của bạn khi mã của bạn thiết lập một trang mới. Bạn chỉ cần await fetch() biết rằng nếu không có mạng thì cuối cùng, lỗi sẽ được gửi đến "navigateerror".

navigation.currentEntry cung cấp quyền truy cập vào mục 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à ô trống 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. Quảng cáo vẫn ở cùng một vị trí. Ngược lại, nếu người dùng nhấn vào Quay lại rồi mở lại 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ữ thanh này, ngay cả ở trạng thái của các mục khác, để dễ dàng chuyển 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

API điều hướng hiển thị khái niệm "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 trên mục nhập nhật ký hiện tại, nhưng không hiển thị trực tiếp với người dùng. Tính năng này cực kỳ giống với nhưng có cải tiến từ history.state trong API Lịch sử.

Trong Navigation API (API Điều hướng), 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, mục này sẽ là undefined.

Trạng thái cài đặt

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 muốn cập nhật trạng thái của mục nhập hiện tại, tốt nhất bạn nên thực hiện một thao tác điều hướng thay thế mục nhập hiện tại:

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

Sau đó, trình nghe sự kiện "navigate" có thể nhận 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 không đồng bộ trạng thái qua navigation.reload({state: newState}), khi đó trình nghe "navigate" có thể áp dụng trạng thái đó. Tuy nhiên, đôi khi sự thay đổi về trạng thái đã được áp dụng đầy đủ vào thời điểm mã của bạn nghe về sự 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 phương thức nhập vào 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 để những thay đổi này được giữ nguyên thông qua các lần tải lại và truyền tải. Bạn có thể thực hiện việc này bằng cách sử dụng updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

Ngoài ra còn có một sự kiện để nghe về thay đổi này:

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

Tuy nhiên, nếu bạn thấy mình phản ứng với các thay đổi về trạng thái trong "currententrychange", bạn có thể chia tách hoặc thậm chí là sao chép mã chuyển giao 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ộ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ữ 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ữ trạng thái đó trong URL. Nếu không, đối tượng trạng thái là lựa chọn phù hợp hơn.

Truy cập tất cả các mục

Tuy nhiên, "mục hiện tại" chưa phải là tất cả. API này cũng cung cấp cách truy cập vào toàn bộ danh sách các mục mà người dùng đã di chuyển qua 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 các mục nhập. Ví dụ: chức năng này có thể được dùng để hiển thị một giao diện người dùng khác dựa trên cách người dùng điều hướng đế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 đó. Với API Lịch sử hiện tại thì bạn không thể làm như vậy.

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 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. (Tất cả các loại thông số kỹ thuật có thể có một phụ lục dài.)

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 kiểu điều hướng đáng chú ý và phức tạp hơn mà bạn nên đề cập đến.

Di chuyển có lập trình

Đầu tiên là hoạt động điều hướng có lập trình, trong đó quá trình đ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.

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

Đây là cách tổng hợp cải tiến các phương thức cũ như location.assign() và bạn bè, cùng với các phương thức pushState()replaceState() của API Lịch sử.

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 trình gọi có thể chờ cho đến khi quá trình chuyển đổi được "cam kết" (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 mà intercept({ handler }) trả về đã hoàn tất hoặc bị từ chối do không thành công hoặc bị giành quyền bởi một thao tác điều hướng khác).

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

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

Cụ thể, info có thể hữu ích trong việc biểu thị một ảnh động cụ thể giúp 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 dấu #hash. Cả hai tuỳ chọn đều hơi bất tiện.) Đá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, chẳng hạn như thông qua nút Quay lại và Tiến lên. 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 từng đề 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(). Phiên bản 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, gửi HTML <form> qua POST là một loại điều hướng đặc biệt và API Điều hướng có thể chặn điều này. Mặc dù có tải trọng bổ sung, 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ụ 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ó tính chất tập trung, nhưng thông số kỹ thuật hiện tại của API Điều hướng không kích hoạt "navigate" trong lần tải đầu tiên của trang. Còn đối với những trang web sử dụng tính năng Hiển thị phía máy chủ (SSR) cho tất cả các trạng thái, điều này có thể không gây ra vấn đề — máy chủ của bạn có thể trả về đúng trạng thái ban đầu, đây là cách nhanh nhất để đưa nội dung đến 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 thêm một chức năng để khởi chạy trang.

Một lựa chọn thiết kế có chủ đích khác của API Điều hướng 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ố ý nghĩa thú vị được nêu thêm trong thông số kỹ thuật, nhưng trên thực tế, sẽ giúp nhà phát triển bớt nhầm lẫn. API Lịch sử trước đây có một số trường hợp hiếm gặp gây nhầm lẫn, chẳng hạn như khả năng hỗ trợ khung và Navigation API được thiết kế lại xử lý các trường hợp hiếm gặp này ngay từ đầu.

Cuối cùng, vẫn chưa nhất trí về việc sửa đổi hoặc sắp xếp lại danh sách các mục nhập mà người dùng đã di chuyển qua theo cách lập trình. Nội dung này hiện đang được thảo luận, nhưng có một lựa chọn có thể là chỉ cho phép xoá: có thể là các mục cũ 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 vai trò là nhà phát triển, tôi có thể:

  • đặt câu hỏi cho người dùng 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 cho 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). Điều này chỉ không thực hiện được với API Lịch sử 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ể dùng thử một bản minh hoạ của Domenic Denicola.

Mặc dù API Lịch sử cổ điển có vẻ đơn giản, nhưng nó không được định nghĩa rõ ràng và có một số lượng lớn vấn đề liên quan đến các trường hợp sai lệch cũng như cách 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 đưa ra ý kiến phản hồi về Navigation API mới.

Tài liệu tham khảo

Xác nhận

Cảm ơn Thomas Steiner, Domenic Denicola và Nate Chapin đã xem xét bài đăng này. Hình ảnh chính của Unsplash, của Jeremy Zero.