ทำให้การกำหนดเส้นทางฝั่งไคลเอ็นต์เป็นมาตรฐานผ่าน API ใหม่ ซึ่งช่วยพลิกโฉมการสร้างแอปพลิเคชันหน้าเว็บเดียวอย่างสมบูรณ์
แอปพลิเคชันหน้าเว็บเดียวหรือ 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
แสดงคำแนะนำ (ซึ่งเกิดขึ้นโดยอัตโนมัติกับฟังก์ชันอะซิงโครนัส) สัญญาดังกล่าวจะบอกเบราว์เซอร์ว่าการนำทางใช้เวลานานเท่าใด และทำงานสำเร็จหรือไม่
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 ใหม่
ข้อมูลอ้างอิง
- WICG/navigation-api
- ตำแหน่งมาตรฐานของ Mozilla
- ความตั้งใจในการสร้างต้นแบบ
- ตรวจสอบ TAG
- รายการสถานะ Chrome
กิตติกรรมประกาศ
ขอขอบคุณ Thomas Steiner, Domenic Denicola และ Nate Chapin สำหรับการรีวิวโพสต์นี้ รูปภาพหลักจาก Unsplash โดย Jeremy Zero