การกำหนดเส้นทางฝั่งไคลเอ็นต์แบบใหม่: Navigation API

ทำให้การกำหนดเส้นทางฝั่งไคลเอ็นต์เป็นมาตรฐานผ่าน API ใหม่ ซึ่งช่วยพลิกโฉมการสร้างแอปพลิเคชันหน้าเว็บเดียวอย่างสมบูรณ์

การรองรับเบราว์เซอร์

  • 102
  • 102
  • x
  • x

แหล่งที่มา

แอปพลิเคชันหน้าเว็บเดียวหรือ SPA ได้รับการกำหนดตามคุณลักษณะหลัก ซึ่งก็คือการเขียนเนื้อหาใหม่แบบไดนามิกขณะที่ผู้ใช้โต้ตอบกับเว็บไซต์ แทนที่จะใช้วิธีการเริ่มต้นในการโหลดหน้าใหม่ทั้งหมดจากเซิร์ฟเวอร์

ในขณะที่ SPA สามารถนำฟีเจอร์นี้มาให้คุณได้ผ่าน API ประวัติ (หรือในบางกรณีคือโดยการปรับส่วน #hash ของเว็บไซต์) แต่ API อันน่าเหลือเชื่อที่พัฒนามาอย่างยาวนานก่อนที่ SPA จะเป็นเรื่องทั่วไป และเว็บก็กำลังเรียกร้องแนวทางใหม่โดยสิ้นเชิง API การนำทางคือ API ที่เสนอซึ่งปรับปรุงพื้นที่นี้ใหม่ทั้งหมด แทนที่จะพยายามแก้ไขขอบขรุขระของ History API เพียงอย่างเดียว (ตัวอย่างเช่น ScrollRestore แพตช์ API ประวัติแทนที่จะพยายามสร้างใหม่)

โพสต์นี้อธิบายเกี่ยวกับ API การนำทางในระดับสูง หากต้องการอ่านข้อเสนอทางเทคนิค ให้ดูรายงานฉบับร่างในที่เก็บ WICG

ตัวอย่างการใช้งาน

ในการใช้ API การนำทาง ให้เริ่มต้นด้วยการเพิ่ม Listener "navigate" ในออบเจ็กต์ navigation ส่วนกลาง โดยพื้นฐานแล้ว เหตุการณ์นี้จะอยู่ในศูนย์กลาง โดยจะเริ่มทำงานสำหรับการนำทางทุกประเภท ไม่ว่าผู้ใช้จะดำเนินการ (เช่น คลิกลิงก์ ส่งแบบฟอร์ม หรือย้อนกลับและไปข้างหน้า) หรือเมื่อมีการทริกเกอร์การนำทางแบบเป็นโปรแกรม (เช่น ผ่านโค้ดของเว็บไซต์) ในกรณีส่วนใหญ่ โค้ดจะช่วยให้โค้ดของคุณลบล้างลักษณะการทำงานเริ่มต้นของเบราว์เซอร์สำหรับการดำเนินการนั้น สำหรับ SPA นั่นหมายถึงการคงผู้ใช้ไว้ในหน้าเดิม และโหลดหรือเปลี่ยนแปลงเนื้อหาของเว็บไซต์

มีการส่ง NavigateEvent ไปยัง Listener ของ "navigate" ซึ่งมีข้อมูลเกี่ยวกับการนำทาง เช่น URL ปลายทาง และช่วยให้คุณตอบกลับการนำทางได้ในที่เดียว Listener "navigate" พื้นฐานอาจมีลักษณะดังนี้

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

คุณจัดการการนำทางได้ 2 วิธี ดังนี้

  • กำลังโทรหา intercept({ handler }) (ตามที่อธิบายไว้ข้างต้น) เพื่อจัดการการนำทาง
  • กำลังโทรหา preventDefault() ซึ่งอาจยกเลิกการนำทางทั้งหมด

ตัวอย่างนี้เรียก intercept() ในเหตุการณ์ เบราว์เซอร์จะเรียก Callback ของ handler ซึ่งควรกำหนดค่าสถานะถัดไปของเว็บไซต์ การดำเนินการนี้จะสร้างออบเจ็กต์การเปลี่ยน navigation.transition ซึ่งโค้ดอื่นๆ สามารถใช้เพื่อติดตามความคืบหน้าของการนำทางได้

โดยปกติแล้วทั้ง intercept() และ preventDefault() จะได้รับอนุญาต แต่มีกรณีที่ไม่สามารถโทรหาได้ คุณไม่สามารถจัดการการนำทางผ่าน intercept() หากการนำทางนั้นเป็นการนำทางแบบข้ามต้นทาง และจะยกเลิกการนำทางผ่าน preventDefault() ไม่ได้หากผู้ใช้กดปุ่มย้อนกลับหรือไปข้างหน้าในเบราว์เซอร์ คุณไม่ควรจะดักจับผู้ใช้ในเว็บไซต์ได้ (เรื่องนี้กำลังมีการพูดถึงใน GitHub)

แม้ว่าคุณจะหยุดหรือขัดขวางการนำทางไม่ได้ แต่เหตุการณ์ "navigate" ก็จะยังคงเริ่มทำงาน ข้อมูลนี้มีข้อมูล ดังนั้นโค้ดอาจบันทึกเหตุการณ์ Analytics เพื่อระบุว่าผู้ใช้ออกจากเว็บไซต์ของคุณ เป็นต้น

เหตุใดจึงต้องเพิ่มกิจกรรมอื่นลงในแพลตฟอร์ม

Listener เหตุการณ์ "navigate" จะรวมการจัดการการเปลี่ยนแปลง URL ภายใน SPA นี่เป็นข้อเสนอที่ยากต่อการใช้ API รุ่นเก่า หากคุณเคยเขียนเส้นทางสำหรับ SPA ของคุณเองโดยใช้ History API คุณอาจเพิ่มโค้ดดังนี้

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

ซึ่งถือว่ายอมรับได้ แต่ยังไม่ใช่ข้อมูลทั้งหมด ลิงก์อาจมาและไปยังหน้าเว็บของคุณ และไม่ใช่วิธีเดียวที่ผู้ใช้จะไปยังหน้าต่างๆ ได้ เช่น อาจส่งแบบฟอร์มหรือแม้กระทั่งใช้แผนที่รูปภาพ หน้าเว็บของคุณอาจรับมือกับปัญหาเหล่านี้ได้ แต่มีความเป็นไปได้มากมายที่สามารถทำให้เข้าใจได้ง่ายขึ้น นั่นคือสิ่งที่ Navigation API ใหม่ทำได้

นอกจากนี้ บริการข้างต้นจะไม่จัดการการนำทางกลับ/ไปข้างหน้า มีอีกกิจกรรมหนึ่งสำหรับกิจกรรมนั้น "popstate"

โดยส่วนตัวแล้ว API ของประวัติมักจะให้ความรู้สึกว่าอาจช่วยส่งเสริมความเป็นไปได้เหล่านี้ อย่างไรก็ตาม การใช้งานจริงมีเพียง 2 ส่วนเท่านั้น นั่นก็คือการตอบสนองเมื่อผู้ใช้กด "กลับ" หรือ "ไปข้างหน้า" ในเบราว์เซอร์ รวมไปถึงการพุชและแทนที่ URL ซึ่งไม่ได้คล้ายกับ "navigate" เพียงแต่ว่าคุณได้ตั้งค่า Listener สำหรับเหตุการณ์การคลิกด้วยตนเอง ดังเช่นที่แสดงข้างต้น

การเลือกวิธีจัดการการนำทาง

navigateEvent มีข้อมูลมากมายเกี่ยวกับการนำทางที่คุณสามารถใช้เพื่อตัดสินใจว่าจะจัดการกับการนำทางหนึ่งๆ อย่างไร

พร็อพเพอร์ตี้ที่สำคัญมีดังนี้

canIntercept
หากเป็นเท็จ คุณจะสกัดกั้นการนำทางไม่ได้ ไม่สามารถดักจับการนำทางข้ามต้นทางและการข้ามเอกสารได้
destination.url
อาจเป็นข้อมูลที่สำคัญที่สุดที่ควรคำนึงถึงเมื่อจัดการการนำทาง
hashChange
เป็นจริงหากการนำทางเป็นเอกสารเดียวกัน และแฮชเป็นเพียงส่วนเดียวของ URL ที่ต่างจาก URL ปัจจุบัน ใน SPA สมัยใหม่ แฮชควรใช้สำหรับการลิงก์ไปยังส่วนต่างๆ ของเอกสารปัจจุบัน ดังนั้น หาก hashChange เป็นจริง คุณก็ไม่จําเป็นต้องแทรกแซงการนําทางนี้
downloadRequest
หากกรณีนี้เป็นจริง การนำทางจะเริ่มต้นโดยลิงก์ที่มีแอตทริบิวต์ download ในกรณีส่วนใหญ่ คุณไม่จำเป็นต้องสกัดกั้นการดำเนินการนี้
formData
หากไม่ใช่ค่าว่าง แสดงว่าการนำทางนี้เป็นส่วนหนึ่งของการส่งแบบฟอร์ม POST อย่าลืมคำนึงถึงเรื่องนี้เมื่อจัดการการนำทาง หากต้องการจัดการเฉพาะการนําทาง GET ให้หลีกเลี่ยงการดักจับการนําทางเมื่อ formData ไม่เป็นค่าว่าง ดูตัวอย่างการจัดการการส่งแบบฟอร์มได้ภายหลังในบทความ
navigationType
นี่คือหนึ่งใน "reload", "push", "replace" หรือ "traverse" หากเป็น"traverse" คุณจะยกเลิกการนำทางนี้ผ่าน preventDefault() ไม่ได้

ตัวอย่างเช่น ฟังก์ชัน shouldNotIntercept ที่ใช้ในตัวอย่างแรกอาจเป็นดังนี้

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

ดักบอล

เมื่อโค้ดของคุณเรียกใช้ intercept({ handler }) จากภายใน Listener "navigate" โค้ดจะแจ้งให้เบราว์เซอร์ทราบว่าขณะนี้กำลังเตรียมหน้าเว็บสำหรับสถานะใหม่ที่อัปเดต และการนำทางอาจใช้เวลาสักครู่

เบราว์เซอร์จะเริ่มต้นโดยบันทึกตำแหน่งการเลื่อนสำหรับสถานะปัจจุบัน เพื่อให้คุณเลือกคืนค่าภายหลังได้ จากนั้นเบราว์เซอร์จะเรียก Callback ของ handler หาก handler แสดงคำแนะนำ (ซึ่งเกิดขึ้นโดยอัตโนมัติกับasync functions) สัญญาดังกล่าวจะบอกเบราว์เซอร์ว่าการนำทางใช้เวลานานเท่าใด และทำงานสำเร็จหรือไม่

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

ด้วยเหตุนี้ API นี้จึงนำเสนอแนวคิดทางอรรถศาสตร์ที่เบราว์เซอร์เข้าใจ กล่าวคือ ปัจจุบันการนำทางของ SPA กำลังเกิดขึ้น เมื่อเวลาผ่านไปจะมีการเปลี่ยนแปลงเอกสารจาก URL และสถานะก่อนหน้าไปเป็นรายการใหม่ ซึ่งมีประโยชน์มากมาย เช่น ความสามารถเข้าถึงได้ง่าย เบราว์เซอร์อาจแสดงจุดเริ่มต้น จุดสิ้นสุด หรือความล้มเหลวในการนำทาง ตัวอย่างเช่น Chrome เปิดใช้งานตัวบ่งชี้การโหลดดั้งเดิมและอนุญาตให้ผู้ใช้โต้ตอบกับปุ่มหยุด (ปัจจุบันสิ่งนี้จะไม่เกิดขึ้นเมื่อผู้ใช้ไปยังส่วนต่างๆ ด้วยปุ่มย้อนกลับ/ไปข้างหน้า แต่จะได้รับการแก้ไขในเร็วๆ นี้)

เมื่อสกัดกั้นการนำทาง URL ใหม่จะมีผลก่อนที่จะมีการเรียก Callback ของ handler หากคุณไม่อัปเดต DOM ทันที การดำเนินการดังกล่าวจะสร้างช่วงเวลาที่เนื้อหาเก่าปรากฏพร้อมกับ URL ใหม่ ซึ่งส่งผลต่อสิ่งต่างๆ เช่น ความละเอียดของ URL สัมพัทธ์ เมื่อดึงข้อมูลหรือโหลดทรัพยากรย่อยใหม่

วิธีทำให้การเปลี่ยน URL ล่าช้าคือการมีการพูดถึงใน GitHub แต่โดยทั่วไปแล้ว เราขอแนะนำให้อัปเดตหน้าเว็บด้วยตัวยึดตำแหน่งบางอย่างสำหรับเนื้อหาที่เข้ามาใหม่ทันที ดังนี้

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

วิธีนี้ไม่เพียงเป็นการแก้ปัญหาเกี่ยวกับ URL เท่านั้น แต่ยังช่วยให้คุณตอบกลับผู้ใช้ได้อย่างทันท่วงทีอีกด้วย

ยกเลิกสัญญาณ

เนื่องจากคุณทำงานแบบไม่พร้อมกันในเครื่องจัดการ intercept() ได้ การนำทางจึงอาจซ้ำซ้อน ซึ่งจะเกิดขึ้นเมื่อ

  • ผู้ใช้คลิกลิงก์อื่น หรือโค้ดบางรายการดำเนินการไปยังส่วนต่างๆ อีกครั้ง ในกรณีนี้ ระบบจะละทิ้งการนำทางเดิมเพื่อไปใช้การนำทางใหม่แทน
  • ผู้ใช้คลิกปุ่ม "หยุด" ในเบราว์เซอร์

ในการจัดการกับความเป็นไปได้เหล่านี้ เหตุการณ์ที่ส่งไปยัง Listener "navigate" จะมีพร็อพเพอร์ตี้ signal ซึ่งก็คือ AbortSignal ดูข้อมูลเพิ่มเติมได้ที่การดึงข้อมูลที่จะยกเลิกได้

พูดง่ายๆ ก็คือ องค์ประกอบนี้จะให้ออบเจ็กต์ที่ทำให้เหตุการณ์เริ่มทำงานเมื่อคุณควรหยุดทำงาน ที่สำคัญ คุณสามารถส่ง AbortSignal ไปยังทุกสายที่คุณโทรไปยัง fetch() ซึ่งจะยกเลิกคำขอเครือข่ายในเที่ยวบินหากมีการนำทางล่วงหน้าไว้ การดำเนินการนี้จะบันทึกแบนด์วิดท์ของผู้ใช้และปฏิเสธ Promise ที่ fetch() แสดงผล ทำให้โค้ดต่อไปนี้ทำงานไม่ได้ เช่น อัปเดต DOM ให้แสดงการนำทางหน้าเว็บที่ไม่ถูกต้องในตอนนี้

นี่คือตัวอย่างก่อนหน้า แต่ที่มี getArticleContent ในบรรทัดแสดงวิธีใช้ AbortSignal กับ 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);
      },
    });
  }
});

การจัดการการเลื่อน

เมื่อคุณintercept()การนำทาง เบราว์เซอร์จะพยายามจัดการการเลื่อนโดยอัตโนมัติ

สำหรับการนำทางไปยังรายการประวัติใหม่ (เมื่อ navigationEvent.navigationType คือ "push" หรือ "replace") หมายความว่าจะเป็นการพยายามเลื่อนไปยังส่วนที่แสดงโดยส่วนย่อยของ URL (บิตหลัง #) หรือรีเซ็ตการเลื่อนกลับไปที่ด้านบนของหน้า

สำหรับการโหลดซ้ำและการข้ามผ่าน จะหมายถึงการคืนค่าตำแหน่งการเลื่อนไปยังตำแหน่งครั้งล่าสุดที่แสดงรายการประวัตินี้

โดยค่าเริ่มต้น เหตุการณ์เช่นนี้จะเกิดขึ้นเมื่อคำมั่นสัญญาที่ handler ส่งคืนไว้ได้รับการแก้ไขแล้ว แต่หากคุณสามารถเลื่อนดูก่อนหน้านี้ได้ คุณสามารถเรียกใช้ 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);
      },
    });
  }
});

หรือจะเลือกไม่ใช้การจัดการการเลื่อนอัตโนมัติทั้งหมดเลยก็ได้ โดยตั้งค่าตัวเลือก scroll ของ intercept() เป็น "manual"

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

โฟกัสการจัดการ

เมื่อแก้ไขคำสัญญาที่ได้จาก handler แล้ว เบราว์เซอร์จะโฟกัสองค์ประกอบแรกที่มีชุดแอตทริบิวต์autofocus หรือองค์ประกอบ <body> หากไม่มีองค์ประกอบใดที่มีแอตทริบิวต์นั้น

คุณเลือกไม่ใช้ลักษณะการทำงานนี้ได้โดยตั้งค่าตัวเลือก focusReset ของ intercept() เป็น "manual"

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

เหตุการณ์ความสำเร็จและล้มเหลว

เมื่อมีการเรียกเครื่องจัดการ intercept() สิ่งที่เกิดขึ้น 1 ใน 2 ข้อต่อไปนี้

  • หาก Promise ที่แสดงผลตอบสนอง (หรือคุณไม่ได้เรียกใช้ intercept()) Navigation API จะเริ่มทำงาน "navigatesuccess" ด้วย Event
  • หาก Promise ที่แสดงผลปฏิเสธ API จะเริ่มทำงาน "navigateerror" ด้วย ErrorEvent

เหตุการณ์เหล่านี้ช่วยให้โค้ดของคุณจัดการกับความสำเร็จหรือความล้มเหลวได้แบบรวมศูนย์ ตัวอย่างเช่น คุณอาจจัดการกับความสำเร็จโดยการซ่อนตัวบ่งชี้ความคืบหน้าที่แสดงก่อนหน้านี้ ดังนี้

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

หรือคุณอาจแสดงข้อความแสดงข้อผิดพลาดต่อไปนี้เมื่อล้มเหลว

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

Listener เหตุการณ์ "navigateerror" ซึ่งได้รับ ErrorEvent มีประโยชน์อย่างยิ่งเพราะรับประกันว่าจะได้รับข้อผิดพลาดจากโค้ดที่กำลังตั้งค่าหน้าใหม่ คุณเพียงแค่await fetch()ทราบว่าหากเครือข่ายไม่พร้อมใช้งาน ข้อผิดพลาดจะถูกกำหนดเส้นทางไปยัง "navigateerror" ในท้ายที่สุด

navigation.currentEntry ให้สิทธิ์เข้าถึงรายการปัจจุบัน นี่คือออบเจ็กต์ที่อธิบายตำแหน่งที่ผู้ใช้อยู่ในขณะนี้ รายการนี้ประกอบด้วย URL ปัจจุบัน ข้อมูลเมตาที่ใช้ระบุรายการนี้ได้เมื่อเวลาผ่านไป และสถานะที่นักพัฒนาแอประบุ

ข้อมูลเมตาประกอบด้วย key ซึ่งเป็นพร็อพเพอร์ตี้สตริงที่ไม่ซ้ำกันของแต่ละรายการ ซึ่งแสดงถึงรายการปัจจุบันและ slot คีย์นี้จะยังคงเหมือนเดิมแม้ว่า URL หรือสถานะของรายการปัจจุบันจะเปลี่ยนไปก็ตาม ลูกค้ายังคงอยู่ในช่องเดิม ในทางกลับกัน หากผู้ใช้กด "กลับ" แล้วเปิดหน้าเดิมอีกครั้ง key จะเปลี่ยนไปเมื่อรายการใหม่นี้สร้างช่องโฆษณาใหม่

สำหรับนักพัฒนาซอฟต์แวร์ key มีประโยชน์เนื่องจาก API การนำทางช่วยให้คุณนำทางผู้ใช้ไปยังรายการที่มีคีย์ตรงกันได้โดยตรง คุณสามารถยึดไว้กับที่ได้ด้วย แม้ในสถานะของรายการอื่นๆ เพื่อให้ข้ามไปมาระหว่างหน้าได้โดยง่าย

// 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;

รัฐ

Navigation API จะแสดงแนวคิดของ "สถานะ" ซึ่งเป็นข้อมูลที่ได้จากนักพัฒนาซอฟต์แวร์และจัดเก็บไว้ในประวัติปัจจุบันอย่างถาวร แต่ผู้ใช้มองไม่เห็นโดยตรง ซึ่งมีความคล้ายคลึงกันอย่างมากกับ history.state ใน History API แต่ได้รับการปรับปรุงให้ดีขึ้น

ใน API การนำทาง คุณสามารถเรียกใช้เมธอด .getState() ของรายการปัจจุบัน (หรือรายการใดก็ได้) เพื่อแสดงผลสำเนาสถานะ

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

โดยค่าเริ่มต้นจะเป็น undefined

สถานะการตั้งค่า

แม้ว่าออบเจ็กต์สถานะอาจมีการเปลี่ยนแปลงได้ แต่การเปลี่ยนแปลงเหล่านั้นจะไม่ได้รับการบันทึกกลับไปพร้อมกับรายการประวัติ ดังนั้น

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

วิธีที่ถูกต้องในการตั้งสถานะคือระหว่างการนำทางสคริปต์

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

โดยที่ newState สามารถเป็นออบเจ็กต์ที่โคลนได้แบบใดก็ได้

หากคุณต้องการอัปเดตสถานะของรายการปัจจุบัน วิธีที่ดีที่สุดคือใช้การนำทางแทนที่รายการปัจจุบัน ดังนี้

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

จากนั้น Listener เหตุการณ์ "navigate" จะรับการเปลี่ยนแปลงนี้ผ่าน navigateEvent.destination ได้

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

การอัปเดตสถานะพร้อมกัน

โดยทั่วไปแล้ว การอัปเดตสถานะแบบไม่พร้อมกันผ่าน navigation.reload({state: newState}) จะดีกว่า ดังนั้นผู้ฟัง "navigate" จะใช้สถานะนั้นได้ อย่างไรก็ตาม บางครั้งการเปลี่ยนแปลงสถานะก็มีผลโดยสมบูรณ์ไปแล้วเมื่อโค้ดของคุณทราบเรื่อง เช่น เมื่อผู้ใช้เปิด/ปิดองค์ประกอบ <details> หรือผู้ใช้เปลี่ยนสถานะการป้อนข้อมูลในแบบฟอร์ม ในกรณีเหล่านี้ คุณอาจต้องอัปเดตสถานะเพื่อให้การเปลี่ยนแปลงเหล่านี้ได้รับการคงไว้ผ่านการโหลดซ้ำและการข้ามผ่าน การดำเนินการดังกล่าวทำได้โดยใช้ updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

นอกจากนี้ ยังมีกิจกรรมให้คุณได้รับทราบเกี่ยวกับการเปลี่ยนแปลงนี้ดังนี้

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

แต่หากพบว่าตนเองตอบสนองต่อการเปลี่ยนแปลงสถานะใน "currententrychange" คุณอาจแยกหรือแม้แต่ทำซ้ำโค้ดกำหนดสถานะระหว่างเหตุการณ์ "navigate" กับเหตุการณ์ "currententrychange" ขณะที่ navigation.reload({state: newState}) ให้คุณจัดการเรื่องดังกล่าวได้ในที่เดียว

สถานะเทียบกับพารามิเตอร์ URL

เนื่องจากสถานะอาจเป็นออบเจ็กต์ที่มีโครงสร้าง ระบบจึงอยากใช้สถานะนี้สำหรับสถานะของแอปพลิเคชันทั้งหมด อย่างไรก็ตาม ในหลายกรณี คุณควรจัดเก็บสถานะนั้นไว้ใน URL

หากคุณอยากให้มีการเก็บรักษาสถานะไว้เมื่อผู้ใช้แชร์ URL กับผู้ใช้รายอื่น ให้จัดเก็บไว้ใน URL มิเช่นนั้น ออบเจ็กต์สถานะจะเป็นตัวเลือกที่ดีกว่า

เข้าถึงรายการทั้งหมด

อย่างไรก็ตาม "รายการปัจจุบัน" ไม่ใช่ทั้งหมด นอกจากนี้ API ยังมอบวิธีเข้าถึงรายการทั้งหมดที่ผู้ใช้สำรวจขณะใช้เว็บไซต์ผ่านการเรียกใช้ navigation.entries() ซึ่งจะแสดงผลอาร์เรย์ภาพรวมของรายการ ซึ่งอาจนำไปใช้เพื่อวัตถุประสงค์ต่างๆ เช่น แสดง UI ที่แตกต่างกันตามวิธีที่ผู้ใช้ไปยังหน้าเว็บหนึ่งๆ หรือเพื่อย้อนดู URL หรือสถานะก่อนหน้า ซึ่งจะดำเนินการไม่ได้เมื่อใช้ API ประวัติปัจจุบัน

นอกจากนั้น คุณยังฟังเหตุการณ์ "dispose" ใน NavigationHistoryEntry แต่ละรายการได้ด้วย ซึ่งจะเริ่มทำงานเมื่อรายการไม่ได้เป็นส่วนหนึ่งของประวัติเบราว์เซอร์แล้ว ซึ่งอาจเป็นส่วนหนึ่งของการทำความสะอาดทั่วไป แต่ก็เกิดขึ้นเมื่อนำทางด้วย ตัวอย่างเช่น ถ้าคุณข้ามกลับ 10 สถานที่ แล้วไปข้างหน้า ประวัติ 10 สถานที่จะถูกทิ้งไป

ตัวอย่าง

เหตุการณ์ "navigate" เริ่มทำงานสำหรับการนำทางทุกประเภทตามที่กล่าวไว้ข้างต้น (จริงๆ แล้วมีภาคผนวกแบบยาวในข้อกำหนดของทุกประเภทที่เป็นไปได้)

แม้ว่าเว็บไซต์จำนวนมากมักจะเป็นกรณีเมื่อผู้ใช้คลิก <a href="..."> แต่ก็มีประเภทการไปยังส่วนต่างๆ ที่สำคัญและซับซ้อนกว่า 2 ประเภทซึ่งควรค่าแก่การที่ครอบคลุม

การไปยังส่วนต่างๆ แบบเป็นโปรแกรม

ประการแรกคือการนำทางแบบเป็นโปรแกรม ซึ่งการนำทางเกิดจากการเรียกใช้เมธอดภายในโค้ดฝั่งไคลเอ็นต์ของคุณ

คุณสามารถโทรหา navigation.navigate('/another_page') ได้จากทุกที่ในโค้ดเพื่อทำให้เกิดการนำทาง สิ่งนี้จะจัดการโดย Listener เหตุการณ์ส่วนกลางที่ลงทะเบียนไว้บน Listener "navigate" และ Listener ส่วนกลางจะเรียกว่าซิงโครนัส

ซึ่งมีวัตถุประสงค์เพื่อการรวมเมธอดเก่าๆ เช่น location.assign() และ Friends ที่ดีขึ้นเข้าด้วยกัน รวมถึงเมธอด pushState() และ replaceState() ของ History API

เมธอด navigation.navigate() จะแสดงออบเจ็กต์ที่มีอินสแตนซ์ Promise 2 รายการใน { committed, finished } การดำเนินการนี้จะอนุญาตให้ผู้เรียกใช้สามารถรอจนกว่าการเปลี่ยนจะ "ดำเนินการ" (URL ที่มองเห็นมีการเปลี่ยนแปลงและ NavigationHistoryEntry ใหม่พร้อมใช้งาน) หรือ "เสร็จสิ้น" (คำมั่นสัญญาทั้งหมดที่ intercept({ handler }) ส่งคืนนั้นเสร็จสมบูรณ์ หรือถูกปฏิเสธเนื่องจากการนำทางอื่นล้มเหลวหรือถูกจองสิทธิ์ไว้)

เมธอด navigate ยังมีออบเจ็กต์ตัวเลือก ซึ่งคุณจะตั้งค่าได้ดังนี้

  • state: สถานะของรายการประวัติใหม่ ตามที่พร้อมใช้งานผ่านเมธอด .getState() ใน NavigationHistoryEntry
  • history: ซึ่งสามารถตั้งค่าเป็น "replace" เพื่อแทนที่รายการประวัติปัจจุบัน
  • info: ออบเจ็กต์ที่จะส่งต่อไปยังเหตุการณ์การนําทางผ่าน navigateEvent.info

โดยเฉพาะอย่างยิ่ง info อาจมีประโยชน์สำหรับการแสดงภาพเคลื่อนไหวที่ทำให้หน้าถัดไปปรากฏขึ้น เป็นต้น (อีกทางเลือกหนึ่งคือตั้งค่าตัวแปรร่วมหรือรวมตัวแปรดังกล่าวไว้เป็นส่วนหนึ่งของ #hash ตัวเลือกทั้งสองดูแปลกไปเล็กน้อย) โปรดทราบว่า info นี้จะไม่เล่นซ้ำหากผู้ใช้ทำให้เกิดการนำทางในภายหลัง เช่น ใช้ปุ่มย้อนกลับและไปข้างหน้า ที่จริงแล้วจะเป็น undefined เสมอในกรณีเหล่านั้น

การสาธิตการเปิดจากซ้ายหรือขวา

navigation ยังมีวิธีการนำทางอื่นๆ อีกหลายวิธี ซึ่งจะแสดงผลออบเจ็กต์ที่มี { committed, finished } เราได้พูดถึง traverseTo() แล้ว (ซึ่งยอมรับ key ที่แสดงถึงรายการเฉพาะในประวัติของผู้ใช้) และ navigate() รวมถึง back(), forward() และ reload() ระบบจะจัดการเมธอดเหล่านี้ทั้งหมดโดย Listener เหตุการณ์ "navigate" ส่วนกลาง เช่นเดียวกับ navigate()

การส่งแบบฟอร์ม

ประการที่ 2 การส่ง HTML <form> ผ่าน POST เป็นการนำทางแบบพิเศษและ Navigation API สามารถสกัดกั้นได้ แม้ว่าจะมีเพย์โหลดเพิ่มเติม แต่ผู้ฟัง "navigate" จะยังคงจัดการการนำทางจากส่วนกลาง

คุณตรวจหาการส่งแบบฟอร์มได้โดยการมองหาพร็อพเพอร์ตี้ formData ใน NavigateEvent ต่อไปนี้เป็นตัวอย่างที่จะเปลี่ยนการส่งแบบฟอร์มเป็นแบบฟอร์มซึ่งจะอยู่ในหน้าปัจจุบันผ่าน 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"
      },
    });
  }
});

มีอะไรขาดหายไป

แม้ว่า Listener เหตุการณ์ของ "navigate" จะเป็นแบบรวมศูนย์ แต่ข้อกำหนดของ API การนำทางปัจจุบันก็ไม่ได้เรียก "navigate" ในการโหลดครั้งแรกของหน้าเว็บ และสำหรับเว็บไซต์ที่ใช้การแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) ในทุกรัฐ สถานะนี้อาจไม่มีปัญหา เซิร์ฟเวอร์ของคุณอาจส่งคืนสถานะเริ่มต้นที่ถูกต้องได้ ซึ่งเป็นวิธีที่เร็วที่สุดในการส่งเนื้อหาให้กับผู้ใช้ แต่เว็บไซต์ที่ใช้ประโยชน์จากโค้ดฝั่งไคลเอ็นต์เพื่อสร้างหน้าเว็บอาจต้องสร้างฟังก์ชันเพิ่มเติมเพื่อเริ่มต้นหน้าเว็บ

ทางเลือกในการออกแบบที่ตั้งใจอีกอย่างหนึ่งของ Navigation API คือการทำงานภายในเฟรมเดียว ซึ่งก็คือหน้าระดับบนสุด หรือ <iframe> เฉพาะหน้าใดเฟรมหนึ่ง การทำแบบนี้มีนัยยะที่น่าสนใจหลายอย่างซึ่งจะระบุไว้ในข้อมูลจำเพาะ แต่ในทางปฏิบัติจะช่วยลดความสับสนของนักพัฒนาแอปได้ API ของประวัติก่อนหน้านี้มีกรณีปัญหาพื้นฐานที่ก่อให้เกิดความสับสน เช่น การรองรับเฟรม และ Navigation API โฉมใหม่จะจัดการกรณีปัญหาเหล่านี้ได้ตั้งแต่ต้น

สุดท้ายนี้ ยังไม่มีความเห็นพ้องกันเกี่ยวกับการแก้ไขแบบเป็นโปรแกรมหรือจัดเรียงรายการรายการใหม่ที่ผู้ใช้นำทางผ่าน กรณีนี้อยู่ระหว่างการหารือ แต่ตัวเลือกหนึ่งอาจอนุญาตให้มีการลบเท่านั้น ได้แก่ รายการในอดีตหรือ "รายการในอนาคตทั้งหมด" รายการหลังจะอนุญาตให้มีการระบุสถานะชั่วคราว เช่น ในฐานะนักพัฒนาซอฟต์แวร์ ฉันสามารถ:

  • ถามคำถามผู้ใช้โดยไปที่ URL หรือสถานะใหม่
  • อนุญาตให้ผู้ใช้ทำงานให้เสร็จ (หรือย้อนกลับ)
  • นำรายการประวัติออกเมื่อทำงานเสร็จ

วิธีนี้เหมาะอย่างยิ่งกับการใช้โมดัลชั่วคราวหรือโฆษณาคั่นระหว่างหน้า เช่น URL ใหม่คือสิ่งที่ผู้ใช้สามารถใช้ท่าทางสัมผัส "ย้อนกลับ" เพื่อออกจากหน้าต่างนั้น แต่ผู้ใช้ก็ไม่สามารถเดินหน้าต่อเพื่อเปิดอีกครั้งได้ (เพราะรายการถูกนำออก) แต่ไม่สามารถทำได้เมื่อใช้ API ประวัติปัจจุบัน

ลองใช้ API การนำทาง

Navigation API พร้อมใช้งานใน Chrome 102 โดยไม่มีแฟล็ก นอกจากนี้ คุณยังลองใช้การสาธิตของ Domenic Denicola ได้ด้วย

แม้ว่า History API แบบเดิมจะดูเรียบง่าย แต่ก็ยังไม่มีการกำหนดที่ชัดเจน และมีปัญหาจำนวนมากเกี่ยวกับกรณีมุมถนน รวมถึงการใช้งานที่แตกต่างกันในแต่ละเบราว์เซอร์ เราหวังว่าคุณจะพิจารณาแสดงความคิดเห็นเกี่ยวกับ Navigation API ใหม่

รายการอ้างอิง

กิตติกรรมประกาศ

ขอขอบคุณ Thomas Steiner, Domenic Denicola และ Nate Chapin สำหรับการรีวิวโพสต์นี้ รูปภาพหลักจาก Unsplash โดย Jeremy Zero