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

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

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

  • Chrome: 102.
  • Edge: 102
  • Firefox: ไม่รองรับ
  • Safari: ไม่รองรับ

แหล่งที่มา

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

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

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

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

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

ระบบจะส่ง NavigateEvent ไปยัง "navigate" listener ซึ่งมีข้อมูลเกี่ยวกับการนําทาง เช่น URL ปลายทาง และช่วยให้คุณตอบสนองต่อการนําทางได้ในที่เดียว ผู้ฟัง "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() ในเหตุการณ์ เบราว์เซอร์จะเรียก handler callback ซึ่งควรกําหนดค่าสถานะถัดไปของเว็บไซต์ ซึ่งจะสร้างออบเจ็กต์การเปลี่ยน 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"

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

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

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

พร็อพเพอร์ตี้หลักๆ มีดังนี้

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

ตัวอย่างเช่น ฟังก์ชัน shouldNotIntercept ที่ใช้ในตัวอย่างที่ 1 อาจมีลักษณะดังนี้

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 แสดงคำสัญญา (ซึ่งเกิดขึ้นโดยอัตโนมัติกับฟังก์ชันอะซิงโครนัส) สัญญาดังกล่าวจะบอกเบราว์เซอร์ว่าการนำทางใช้เวลานานเท่าใด และทำงานสำเร็จหรือไม่

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() ได้ การนำทางจึงอาจซ้ำซ้อน ซึ่งกรณีนี้จะเกิดขึ้นเมื่อ

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

เหตุการณ์ที่ส่งไปยัง "navigate" listener มีพร็อพเพอร์ตี้ 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 Fragment (ส่วนที่อยู่หลัง #) หรือรีเซ็ตการเลื่อนไปที่ด้านบนของหน้า

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

โดยค่าเริ่มต้น การดำเนินการนี้จะเริ่มต้นขึ้นเมื่อ Promise ที่ 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() สิ่งใดสิ่งหนึ่งต่อไปนี้จะเกิดขึ้น

  • หาก 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 ซึ่งเป็นพร็อพเพอร์ตี้สตริงที่ไม่ซ้ำกันของรายการแต่ละรายการ ซึ่งแสดงถึงรายการปัจจุบันและช่องของรายการ คีย์นี้จะยังคงเหมือนเดิมแม้ว่า 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 มาก แต่มีการปรับปรุง

ใน Navigation 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" Listener ใช้สถานะนั้นได้ อย่างไรก็ตาม บางครั้งการเปลี่ยนแปลงสถานะจะมีผลแล้วโดยสมบูรณ์เมื่อโค้ดของคุณรับรู้ถึงการเปลี่ยนแปลง เช่น เมื่อผู้ใช้เปิด/ปิดองค์ประกอบ <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 ก่อนหน้าหรือสถานะของ URL เหล่านั้น ซึ่ง History API ปัจจุบันทําไม่ได้

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

ตัวอย่าง

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

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

การนำทางแบบเป็นโปรแกรม

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

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

วิธีการนี้มีไว้เพื่อการรวมข้อมูลของวิธีการเก่าๆ เช่น location.assign() และเพื่อน รวมถึงวิธีการ pushState() และ replaceState() ของ History API ที่ปรับปรุงแล้ว

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

เมธอด navigate ยังมีออบเจ็กต์ options ที่คุณตั้งค่าสิ่งต่อไปนี้ได้

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

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

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

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

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

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

คุณสามารถตรวจหาการส่งแบบฟอร์มได้โดยมองหาพร็อพเพอร์ตี้ 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"
      },
    });
  }
});

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

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

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

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

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

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

ลองใช้ Navigation API

Navigation API พร้อมใช้งานใน Chrome 102 โดยไม่ต้องใช้ Flag นอกจากนี้ คุณยังลองใช้เดโมโดย Domenic Denicola ได้ด้วย

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

ข้อมูลอ้างอิง

ขอขอบคุณ

ขอขอบคุณ Thomas Steiner, Domenic Denicola และ Nate Chapin ที่ตรวจสอบโพสต์นี้