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

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

แซม โทโรกู๊ด
แซม โทโรกู๊ด
เจค อาร์ชิบาลด์
เจค อาร์ชิบาลด์

การสนับสนุนเบราว์เซอร์

  • 102
  • 102
  • x
  • x

แหล่งที่มา

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

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

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

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

หากต้องการใช้ Navigation 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() ในเหตุการณ์ เบราว์เซอร์จะเรียกใช้โค้ดเรียกกลับ 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"

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

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

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

คุณสมบัติหลักๆ ได้แก่

canIntercept
หากตั้งค่าไม่ถูกต้อง คุณจะสกัดกั้นการนำทางไม่ได้ ระบบไม่สามารถสกัดกั้นการไปยังส่วนต่างๆ แบบข้ามต้นทางและการส่งข้ามเอกสารได้
destination.url
นี่น่าจะเป็นข้อมูลที่สำคัญที่สุดที่ควรพิจารณาเมื่อต้องจัดการกับการนำทาง
hashChange
เป็นจริงหากการนำทางเป็นเอกสารเดียวกัน และแฮชเป็นส่วนเดียวของ URL ที่แตกต่างจาก URL ปัจจุบัน ใน SPA ยุคใหม่ ควรใช้แฮชสำหรับลิงก์ไปยังส่วนต่างๆ ของเอกสารปัจจุบัน ดังนั้น หาก hashChange เป็นจริง คุณก็ไม่ต้องขัดขวางการนำทางนี้
downloadRequest
หากเป็นจริง แสดงว่าการนำทางเริ่มต้นจากลิงก์ที่มีแอตทริบิวต์ download ในกรณีส่วนใหญ่ คุณไม่จำเป็นต้องขัดขวางกระบวนการนี้
formData
หากไม่เป็น Null การนำทางนี้จะเป็นส่วนหนึ่งของการส่งแบบฟอร์ม 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" จะบอกให้เบราว์เซอร์ทราบว่ากำลังเตรียมหน้าเว็บสำหรับสถานะที่อัปเดตใหม่ และการนำทางอาจใช้เวลาสักครู่

เบราว์เซอร์จะเริ่มต้นโดยการจับตำแหน่งการเลื่อนสำหรับสถานะปัจจุบัน เพื่อให้คุณสามารถเลือกคืนค่าได้ในภายหลัง จากนั้นจะเรียกใช้โค้ดเรียกกลับ 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 และสถานะก่อนหน้าเป็น URL ใหม่ การทำเช่นนี้มีประโยชน์อย่างยิ่ง เช่น การเข้าถึงได้ง่าย เพราะเบราว์เซอร์สามารถแสดงจุดเริ่มต้น จุดสิ้นสุด หรือที่อาจเกิดความล้มเหลวในการนำทาง ตัวอย่างเช่น Chrome จะเปิดใช้งานสัญญาณบอกสถานะการโหลดของระบบและอนุญาตให้ผู้ใช้โต้ตอบกับปุ่มหยุด (ในปัจจุบันจะไม่เกิดขึ้นเมื่อผู้ใช้นำทางด้วยปุ่มย้อนกลับ/ไปข้างหน้า แต่จะแก้ไขในเร็วๆ นี้)

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

  • หาก 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 จะมีประโยชน์เนื่องจาก Navigation 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}) จากนั้น Listener "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 หรือสถานะก่อนหน้า การดำเนินการนี้จะทำไม่ได้หากใช้ 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 ใหม่พร้อมใช้งาน) หรือ "เสร็จสมบูรณ์" (สัญญาทั้งหมดที่ intercept({ handler }) ส่งคืนมาจะสมบูรณ์ หรือถูกปฏิเสธเนื่องจากความล้มเหลวหรือถูกขัดจังหวะโดยการนำทางอื่น)

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

  • 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" แบบรวมศูนย์

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

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

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

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

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

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

ลองใช้ Navigation API

Navigation API มีอยู่ใน Chrome 102 โดยไม่มีการแจ้งว่าไม่เหมาะสม หรือจะลองใช้เดโมจาก Domenic Denicola ก็ได้

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

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

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

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