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

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, cải tiến toàn diện việc tạo các ứng dụng trang đơn.

Jake Archibald
Jake Archibald

Hỗ trợ trình duyệt

  • 102
  • 102
  • x
  • x

Nguồn

Các ứng dụng trang đơn, hay SPA, được xác định bằng tính năng cốt lõi: tự động ghi lại nội dung của các ứng dụng đó khi người dùng tương tác với trang web, thay vì sử dụng phương pháp mặc định là 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 API Lịch sử (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 khó hiểu được phát triển từ lâu trước khi SPA trở thành tiêu chuẩn và web đang mong muốn một phương pháp tiếp cận hoàn toàn mới. Navigation API là một API được đề xuất sẽ thay đổi hoàn toàn 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ụ: tính năng Khôi phục Cuộn đã vá API Lịch sử thay vì cố gắng phát minh lại.)

Bài đăng này mô tả API điều hướng ở cấp cao. Nếu bạn muốn đọ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 (API Điều hướng), hãy bắt đầu bằng cách thêm trình nghe "navigate" vào đố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 hình đ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, quay lại và tiến) hoặc 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à qua mã trang web của bạn). Trong hầu hết các trường hợp, chính sách này cho phép mã của bạn ghi đè hành vi mặc định của trình duyệt cho tác vụ đó. Đố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 trang web.

NavigateEvent được chuyển đến trình nghe "navigate" chứa thông tin về quá trình điều hướng, chẳng hạn như URL đích, và cho phép bạn phản hồi thông tin đ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ý việc điều hướng.
  • Gọi preventDefault() để 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ể sử dụng để theo dõi tiến trình điều hướng.

Cả intercept()preventDefault() thường được phép, nhưng có những 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 qua intercept() nếu đó là một yêu cầu điều hướng nhiều nguồn gốc. Bạn cũng không thể huỷ thao tác điều hướng qua preventDefault() nếu người dùng đang nhấn nút Tiến hoặc Quay lại trong trình duyệt của họ. 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 quá trình điều hướng, sự kiện "navigate" vẫn sẽ kích hoạt. Mã này mang tính thông tin, do đó, mã của bạn có thể ghi nhật ký 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 từng viết mã định tuyến cho SPA của riêng mình bằng API Lịch sử, thì có thể bạn đã thêm đoạn 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));

Câu trả lời này bình thường nhưng chưa đầy đủ. Các đường liên kết có thể đến và đi trên trang của bạn, và chúng 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 vấn đề này, nhưng vẫn còn rất nhiều khả năng có thể được đơn giản hóa — một điều mà API Điều hướng mới đạt được.

Ngoài ra, các thao tác trên không xử lý thao tác tiến/lùi. Có một sự kiện khác cho yêu cầu đó, "popstate".

Cá nhân, API Lịch sử thường cảm thấy hữu ích theo cách nào đó để giải quyết những khả năng này. Tuy nhiên, API này chỉ có hai khu vực nền tảng, đó là phản hồi khi người dùng nhấn vào Quay lại hoặc Chuyển tiếp trong trình duyệt, cùng với đẩy và thay thế URL. API này không giống với "navigate", ngoại trừ việc bạn thiết lập trình nghe theo cách thủ công cho các sự kiện nhấp, 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ề 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 sai, bạn không thể chặn hoạt động điều hướng. Không thể chặn hoạt động điều hướng trên nhiều nguồn gốc và hoạt động truyền tải trên nhiều tài liệu.
destination.url
Có lẽ là thông tin quan trọng nhất cần cân nhắc khi điều hướng.
hashChange
Đúng nếu thành phần đ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, 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à đúng (true), thì bạn có thể không cần chặn sự điều hướng này.
downloadRequest
Nếu đúng là như vậy thì quá trình đ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 luồng thực thi.
formData
Nếu giá trị này không rỗng, thì tức là hoạt động điều hướng này nằm trong quá trình gửi biểu mẫu POST. Hãy nhớ tính đến điều này khi điều hướng. Nếu bạn chỉ muốn xử lý các thao tác 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.
navigationType
Đây là một trong các giá trị "reload", "push", "replace" hoặc "traverse". Nếu giá trị là "traverse", thì bạn không thể huỷ thao tác điều hướng qua preventDefault().

Ví dụ: hàm shouldNotIntercept 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ừ bên trong trình nghe "navigate", mã sẽ thông báo cho trình duyệt rằng hiện đ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 chụp vị trí cuộn cho trạng thái hiện tại để bạn có thể khôi phục sau này (không bắt buộc). Sau đó, trình duyệt sẽ gọi lệnh gọi lại handler. Nếu handler của bạn trả về một lời hứa (điều này tự động xảy ra với async functions), thì lời hứa đó sẽ cho trình duyệt biết thời gian điều hướng 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 giới thiệu một khái niệm ngữ nghĩa mà trình duyệt hiểu được: theo thời gian, hoạt động điều hướng SPA đang diễn ra và thay đổi tài liệu từ URL và trạng thái trước đó sang một URL mới. Điều này có thể mang lại một số lợi ích, bao gồm khả năng hỗ trợ tiếp cận: trình duyệt có thể cho thấy điểm bắt đầu, phần kết thúc hoặc lỗi có thể xảy ra của quá trình đ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 di chuyển qua các nút tiến/lùi, 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, việc này sẽ tạo ra một khoảng thời gian mà 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 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 thường thì bạn nên cập nhật ngay trang 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 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 trả lời người dùng ngay lập tức.

Huỷ 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 điều hướng có thể trở nên 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 thao tác điều hướng khác. Trong trường hợp này, điều hướng cũ sẽ bị bỏ qua và được thay thế bằng đ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 chuyển đến trình nghe "navigate" chứa thuộc tính signal, đó là AbortSignal. Để biết thêm thông tin, hãy xem phần Tìm nạp có thể hủy bỏ.

Phiên bản ngắn gọn là về cơ bản, tính năng này cung cấp một đối tượng sẽ kích hoạt sự kiện khi bạn dừng công việc. Đáng chú ý là bạn có thể chuyển AbortSignal đến bất kỳ lệnh gọi nào bạn thực hiện đến fetch(). Thao tác này sẽ huỷ các yêu cầu mạng đang diễn ra nếu hoạt động điều hướng bị giành quyền. Việ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 những hành động như cập nhật DOM để hiển thị điều hướng trang hiện không hợp lệ.

Đây là ví dụ trước, nhưng với getArticleContent cùng dòng, cho thấy cách AbortSignal có thể được sử dụng 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 thao tác điều hướng, trình duyệt sẽ tìm cách xử lý việc cuộn tự động.

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

Đối với việc 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í lần gần nhất mà mục lịch sử này được hiển thị.

Theo mặc định, điều này xảy ra sau khi handler giải quyết lời hứa được trả về. Tuy nhiên, 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 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 handler trả về lời hứa được phân giải, trình duyệt sẽ lấy tiêu điểm là phần tử đầu tiên bằng cách đặt 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 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à thất bại

Khi trình xử lý intercept() được gọi, một trong 2 đ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.

Những sự kiện này giú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 được 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:

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 sẽ nhận mọi lỗi từ đoạn 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 thì 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à vị trí của mục nhập đó. Khoá này 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 ở nguyên vị trí đó. Ngược lại, nếu người dùng nhấn vào Quay lại rồi mở lại chính trang đó, key sẽ thay đổi khi mục nhập mới này tạo 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 trực tiếp điều hướng người dùng đến một mục nhập có khoá trùng khớp. Bạn có thể giữ thẻ này, ngay cả trong trạng thái của các mục nhập 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 thể hiện khái niệm "trạng thái", là thông tin do nhà phát triển cung cấp và đượ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 trực tiếp nhìn thấy thông tin này. Tính năng này cực kỳ giống với history.state trong API Lịch sử 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 hiện tại (hoặc mục bất kỳ) để 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ài đặt

Mặc dù bạn có thể thay đổi đối tượng trạng thái, nhưng những thay đổi đó không được lưu lại 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 chính xác để đặt trạng thái là trong khi di chuyển 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 sao chép nào.

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 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 biết 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 một cách đồng bộ

Nhìn chung, bạn nên cập nhật trạng thái không đồng bộ qua navigation.reload({state: newState}) để trình nghe "navigate" có thể áp dụng trạng thái đó. Tuy nhiên, đôi khi sự thay đổi trạng thái đã được áp dụng đầy đủ vào thời điểm mã biết đến, chẳng hạn như khi người dùng chuyển đổi một phần tử <details> hoặc khi người dùng thay đổi trạng thái của mục nhập 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 khi 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});

Bạn cũng có thể xem sự kiện sau đây 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ể sẽ phân tách hoặc thậm chí là sao chép mã phân phối 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ý việc này ở cùng một nơi.

Thông số trạng thái so với thông số URL

Vì trạng thái có thể là một đối tượng có cấu trúc, nên bạn sẽ muốn sử dụng nó cho toàn bộ 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à tuỳ chọn tốt hơn.

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

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

Bạn cũng có thể theo dõi một sự kiện "dispose" trên từng NavigationHistoryEntry. Sự kiện này đượ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 trong quá trình điều hướng. Ví dụ: nếu bạn quay lại 10 địa điểm, sau đó tiến lên, 10 mục nhập nhật ký đó sẽ bị loại bỏ.

Ví dụ

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

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

Đầu tiên là tính năng điều hướng có lập trình, trong đó việc đ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 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ẽ được 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ộ.

API này nhằm mụ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 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 "cam kết" (URL hiển thị đã thay đổi và có NavigationHistoryEntry mới) hoặc "hoàn tất" (tất cả lời hứa được intercept({ handler }) trả về đã hoàn tất hoặc bị từ chối do không thành công hoặc bị một thành phần điều hướng khác giành quyền).

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

  • state: trạng thái của mục nhập nhật ký mới, có thể sử dụ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 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. (Bạn có thể đặt một biến toàn cục hoặc đưa biến đó vào hàm #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 kích hoạt thao tác di chuyển, chẳng hạn như 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ạ 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(). Trong đó cũng bao gồm back(), forward()reload(). Các phương thức này đều được trình nghe sự kiện "navigate" tập trung xử lý (giống như navigate()).

Gửi biểu mẫu

Thứ hai, việc gửi <form> HTML qua POST là một loại điều hướng đặc biệt và Navigation API có thể chặn loại điều hướng này. Mặc dù lớp này có một 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 hành động gửi biểu mẫu bằng cách tìm thuộc tính formData trên NavigateEvent. Sau đây là ví dụ đơn giản giúp chuyển mọi lượt gửi biểu mẫu thành biểu mẫu vẫn ở 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. Đối với các trang web sử dụng tính năng Hiển thị phía máy chủ (SSR) cho tất cả các tiểu bang, đ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ề đúng trạng thái ban đầu. Đây là cách nhanh nhất để đưa nội dung cho người dùng. Tuy nhiên, những 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 tạo trang.

Một lựa chọn thiết kế có chủ ý khác của Navigation API (API Điều hướng) là API này chỉ hoạt động trong một khung duy nhất, nghĩa là trang cấp cao nhất hoặc một <iframe> cụ thể. Điều này có nhiều hàm ý thú vị được ghi rõ hơn nữa trong quy cách, nhưng trong thực tế, sẽ làm giảm sự nhầm lẫn của nhà phát triển. API Lịch sử trước đây có một số trường hợp hiếm khó hiểu, chẳng hạn như khả năng hỗ trợ khung hình và Navigation API được thiết kế lại sẽ 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 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 nhập mà người dùng đã di chuyển qua bằng cách lập trình. Nội dung này hiện đang được thảo luận, nhưng có thể chỉ cho phép xóa: mục nhập cũ hoặc "tất cả mục nhập trong tương lai". Chính sá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ể:

  • đặ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 tất công việc của họ (hoặc quay lại)
  • xoá mục nhập nhật ký khi hoàn thành một việc cần làm

Đây có thể là lựa chọn hoàn hảo cho các phương thức hoặc quảng cáo xen kẽ tạm thời: URL mới là thứ 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 quay lại để mở lại (vì mục nhập đã bị xoá). Điều này chỉ là không thể với API Lịch sử hiện tại.

Dùng thử Navigation API

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ử bản minh hoạ của Domenic Denicola.

Mặc dù API lịch sử có vẻ đơn giản, nhưng API này không được xác định rõ ràng và có một số lượng lớn vấn đề xung quanh các trường hợp xảy ra không như dự kiến cũng như cách API đượ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 đư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 trong video Unsplash của Jeremy Zero.