การกำหนดเส้นทางฝั่งไคลเอ็นต์ให้เป็นมาตรฐานผ่าน API ใหม่ล่าสุด ซึ่งยกเครื่องการสร้างแอปพลิเคชันหน้าเว็บเดียวโดยสิ้นเชิง
แอปพลิเคชันแบบหน้าเดียวหรือ SPA มีฟีเจอร์หลักคือการเขียนเนื้อหาใหม่แบบไดนามิกเมื่อผู้ใช้โต้ตอบกับเว็บไซต์ แทนที่จะใช้วิธีเริ่มต้นในการโหลดหน้าใหม่ทั้งหมดจากเซิร์ฟเวอร์
แม้ว่า SPA จะช่วยให้คุณใช้ฟีเจอร์นี้ผ่าน History API ได้ (หรือในบางกรณีที่จำกัดโดยการปรับส่วน #hash ของเว็บไซต์) แต่ก็เป็น API ที่ซับซ้อนซึ่งพัฒนาขึ้นนานก่อนที่ SPA จะกลายเป็นมาตรฐาน และเว็บก็ต้องการแนวทางใหม่โดยสิ้นเชิง Navigation API เป็น API ที่เสนอซึ่งยกเครื่องพื้นที่นี้ใหม่ทั้งหมด แทนที่จะพยายามแก้ไขข้อบกพร่องของ History API (เช่น Scroll Restoration แก้ไข History API แทนที่จะพยายามสร้างขึ้นมาใหม่)
โพสต์นี้อธิบาย Navigation API ในระดับสูง หากต้องการอ่านข้อเสนอทางเทคนิค โปรดดูรายงานฉบับร่างในที่เก็บ WICG
ตัวอย่างการใช้
หากต้องการใช้ Navigation API ให้เริ่มด้วยการเพิ่ม Listener "navigate" ในออบเจ็กต์ navigation ทั่วโลก
เหตุการณ์นี้รวมศูนย์โดยพื้นฐาน ซึ่งจะทํางานสําหรับการนําทางทุกประเภท ไม่ว่าผู้ใช้จะดําเนินการ (เช่น คลิกที่ลิงก์ ส่งแบบฟอร์ม หรือย้อนกลับและไปข้างหน้า) หรือเมื่อมีการเรียกใช้การนําทางโดยโปรแกรม (เช่น ผ่านโค้ดของเว็บไซต์)
ในกรณีส่วนใหญ่ การตั้งค่านี้จะช่วยให้โค้ดของคุณลบล้างลักษณะการทำงานเริ่มต้นของเบราว์เซอร์สำหรับการดำเนินการนั้นได้
สำหรับ SPA การดำเนินการดังกล่าวอาจหมายถึงการให้ผู้ใช้อยู่ในหน้าเว็บเดียวกันและโหลดหรือเปลี่ยนเนื้อหาของเว็บไซต์
ระบบจะส่ง NavigateEvent ไปยังเครื่องมือฟัง "navigate" ซึ่งมีข้อมูลเกี่ยวกับการนำทาง เช่น URL ปลายทาง และช่วยให้คุณตอบสนองต่อการนำทางได้ในที่เดียว
"navigate" ผู้ฟัง"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 เพื่อระบุว่าผู้ใช้กําลังจะออกจากเว็บไซต์
เหตุใดจึงต้องเพิ่มกิจกรรมอื่นลงในแพลตฟอร์ม
"navigate" Listener เหตุการณ์จะรวมการจัดการการเปลี่ยนแปลง 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- หากไม่ใช่ค่าว่าง แสดงว่าการนำทางนี้เป็นส่วนหนึ่งของการส่งแบบฟอร์ม POST
โปรดคำนึงถึงเรื่องนี้เมื่อจัดการการนำทาง
หากต้องการจัดการการไปยัง GET เท่านั้น ให้หลีกเลี่ยงการสกัดกั้นการไปยังส่วนต่างๆ ที่
formDataไม่ใช่ null ดูตัวอย่างการจัดการการส่งแบบฟอร์มได้ในส่วนท้ายของบทความ 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 Callback
หาก handler แสดงผล Promise (ซึ่งจะเกิดขึ้นโดยอัตโนมัติกับฟังก์ชันแบบอะซิงโครนัส) Promise นั้นจะบอกเบราว์เซอร์ว่าการนำทางใช้เวลานานเท่าใดและสำเร็จหรือไม่
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 ที่เกี่ยวข้องเมื่อดึงข้อมูลหรือโหลดทรัพยากรย่อยใหม่
เรากำลังพูดคุยกันใน GitHub เกี่ยวกับวิธีเลื่อนการเปลี่ยนแปลง URL แต่โดยทั่วไปแล้ว เราขอแนะนำให้อัปเดตหน้าเว็บทันทีด้วยตัวยึดตำแหน่งบางประเภทสำหรับเนื้อหาที่กำลังจะเข้ามา
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 (ส่วนหลังจาก #) หรือรีเซ็ตการเลื่อนไปที่ด้านบนของหน้า
สำหรับการโหลดซ้ำและการข้าม การดำเนินการนี้หมายถึงการคืนค่าตำแหน่งการเลื่อนไปยังตำแหน่งที่แสดงรายการประวัตินี้ครั้งล่าสุด
โดยค่าเริ่มต้น การดำเนินการนี้จะเกิดขึ้นเมื่อ 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() {
// …
},
});
การจัดการโฟกัส
เมื่อ Promise ที่ส่งคืนโดย 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}`);
});
"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'});
จากนั้นเครื่องมือฟังเหตุการณ์ "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 ก่อนหน้าหรือสถานะของ URL เหล่านั้น
ซึ่งทำไม่ได้ด้วย History API ปัจจุบัน
นอกจากนี้ คุณยังฟัง"dispose" event ใน NavigationHistoryEntry แต่ละรายการได้ด้วย ซึ่งจะทริกเกอร์เมื่อรายการไม่ได้เป็นส่วนหนึ่งของประวัติเบราว์เซอร์อีกต่อไป ซึ่งอาจเกิดขึ้นได้ในขั้นตอนการล้างข้อมูลทั่วไป แต่ก็อาจเกิดขึ้นเมื่อนำทางด้วย ตัวอย่างเช่น หากคุณย้อนกลับไป 10 ที่ แล้วไปยังที่ถัดไป ระบบจะทิ้งรายการประวัติ 10 รายการนั้น
ตัวอย่าง
"navigate" เหตุการณ์จะทริกเกอร์สำหรับการนำทางทุกประเภทตามที่กล่าวไว้ข้างต้น
(จริงๆ แล้วมีภาคผนวกยาวในข้อกำหนดของประเภทที่เป็นไปได้ทั้งหมด)
แม้ว่าสำหรับเว็บไซต์จำนวนมาก กรณีที่พบบ่อยที่สุดคือเมื่อผู้ใช้คลิก <a href="..."> แต่ก็มีประเภทการนำทางที่ซับซ้อนกว่า 2 ประเภทที่ควรกล่าวถึง
การนำทางแบบเป็นโปรแกรม
อย่างแรกคือการนำทางแบบเป็นโปรแกรม ซึ่งการนำทางเกิดจากการเรียกใช้เมธอดภายในโค้ดฝั่งไคลเอ็นต์
คุณเรียกใช้ navigation.navigate('/another_page') จากที่ใดก็ได้ในโค้ดเพื่อทําให้เกิดการนําทาง
ซึ่งจะได้รับการจัดการโดย Listener เหตุการณ์ส่วนกลางที่ลงทะเบียนไว้ใน "navigate" Listener และระบบจะเรียกใช้ Listener ส่วนกลางแบบพร้อมกัน
ซึ่งมีจุดประสงค์เพื่อปรับปรุงการรวบรวมข้อมูลจากวิธีการเดิม เช่น location.assign() และอื่นๆ รวมถึงวิธีการ pushState() และ replaceState() ของ History API
เมธอด navigation.navigate() จะแสดงผลออบเจ็กต์ซึ่งมีอินสแตนซ์ Promise 2 รายการใน { committed, finished }
ซึ่งจะช่วยให้ผู้เรียกใช้รอจนกว่าการเปลี่ยนเส้นทางจะ "ยืนยัน" (URL ที่มองเห็นได้มีการเปลี่ยนแปลงและมี NavigationHistoryEntry ใหม่) หรือ "เสร็จสิ้น" (สัญญาที่ intercept({ handler }) ส่งคืนทั้งหมดเสร็จสมบูรณ์ หรือถูกปฏิเสธเนื่องจากล้มเหลวหรือถูกการนำทางอื่นขัดจังหวะ)
เมธอด navigate ยังมีออบเจ็กต์ตัวเลือกที่คุณตั้งค่าต่อไปนี้ได้ด้วย
state: สถานะของรายการประวัติใหม่ตามที่ใช้ได้ผ่านเมธอด.getState()ในNavigationHistoryEntryhistory: ซึ่งตั้งค่าเป็น"replace"เพื่อแทนที่รายการประวัติปัจจุบันได้info: ออบเจ็กต์ที่จะส่งไปยังเหตุการณ์นำทางผ่านnavigateEvent.info
โดยเฉพาะอย่างยิ่ง info อาจมีประโยชน์ในการระบุภาพเคลื่อนไหวที่ทำให้หน้าถัดไปปรากฏขึ้น เป็นต้น
(อีกทางเลือกหนึ่งคือการตั้งค่าตัวแปรส่วนกลางหรือรวมไว้เป็นส่วนหนึ่งของ #hash ทั้ง 2 ตัวเลือกนี้อาจดูแปลกๆ หน่อย)
โปรดทราบว่าระบบจะไม่เล่นinfoนี้ซ้ำหากผู้ใช้ทำให้เกิดการนำทางในภายหลัง เช่น ผ่านปุ่มย้อนกลับและไปข้างหน้า
ซึ่งในกรณีดังกล่าว undefined จะเป็นเช่นนั้นเสมอ
navigation ยังมีวิธีการนำทางอื่นๆ อีกหลายวิธี ซึ่งทั้งหมดจะแสดงผลออบเจ็กต์ที่มี { committed, finished }
ฉันได้พูดถึง traverseTo() (ซึ่งยอมรับ key ที่ระบุรายการที่เฉพาะเจาะจงในประวัติของผู้ใช้) และ navigate() ไปแล้ว
นอกจากนี้ ยังรวมถึง back(), forward() และ reload() ด้วย
เมธอดเหล่านี้ทั้งหมดจะได้รับการจัดการเช่นเดียวกับ navigate() โดย "navigate" Listener เหตุการณ์ส่วนกลาง
การส่งแบบฟอร์ม
ประการที่ 2 การส่ง HTML ผ่าน POST เป็นการนำทางประเภทพิเศษ และ Navigation API สามารถสกัดกั้นได้<form>
แม้ว่าจะมีเพย์โหลดเพิ่มเติม แต่"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" Event Listener จะมีลักษณะเป็นแบบรวมศูนย์ แต่ข้อกําหนด Navigation API ปัจจุบันไม่ได้ทริกเกอร์ "navigate" ในการโหลดหน้าเว็บครั้งแรก
และสำหรับเว็บไซต์ที่ใช้การแสดงผลฝั่งเซิร์ฟเวอร์ (SSR) สำหรับทุกสถานะ ก็อาจไม่มีปัญหาอะไร เนื่องจากเซิร์ฟเวอร์จะแสดงสถานะเริ่มต้นที่ถูกต้อง ซึ่งเป็นวิธีที่เร็วที่สุดในการแสดงเนื้อหาต่อผู้ใช้
แต่เว็บไซต์ที่ใช้ประโยชน์จากโค้ดฝั่งไคลเอ็นต์เพื่อสร้างหน้าเว็บอาจต้องสร้างฟังก์ชันเพิ่มเติมเพื่อเริ่มต้นหน้าเว็บ
อีกตัวเลือกการออกแบบที่ตั้งใจของ Navigation API คือการทำงานภายในเฟรมเดียวเท่านั้น ซึ่งก็คือหน้าเว็บระดับบนสุดหรือ <iframe> ที่เฉพาะเจาะจงรายการเดียว
ซึ่งมีผลที่น่าสนใจหลายอย่างที่ระบุไว้เพิ่มเติมในข้อกำหนด แต่ในทางปฏิบัติแล้วจะช่วยลดความสับสนของนักพัฒนาซอฟต์แวร์
History API ก่อนหน้านี้มีกรณีขอบที่สร้างความสับสนหลายอย่าง เช่น การรองรับเฟรม และ Navigation API ที่ปรับปรุงใหม่จะจัดการกรณีขอบเหล่านี้ตั้งแต่เริ่มต้น
สุดท้ายนี้ ยังไม่มีข้อตกลงเกี่ยวกับการแก้ไขหรือจัดเรียงรายการที่ผู้ใช้ไปยังส่วนต่างๆ โดยอัตโนมัติ ขณะนี้เรากำลังพิจารณาเรื่องนี้ แต่อาจมีตัวเลือกหนึ่งคืออนุญาตให้ลบเท่านั้น ไม่ว่าจะเป็นรายการในอดีตหรือ "รายการทั้งหมดในอนาคต" ซึ่งจะอนุญาตให้ใช้สถานะชั่วคราว เช่น ในฐานะนักพัฒนาแอป ฉันสามารถทำสิ่งต่อไปนี้ได้
- ถามคำถามผู้ใช้โดยไปที่ URL หรือสถานะใหม่
- อนุญาตให้ผู้ใช้ทำงานให้เสร็จ (หรือกลับไป)
- นำรายการประวัติออกเมื่อทำงานเสร็จ
ซึ่งเหมาะอย่างยิ่งสำหรับโมดอลหรือโฆษณาคั่นระหว่างหน้าชั่วคราว เนื่องจาก URL ใหม่เป็น URL ที่ผู้ใช้สามารถใช้ท่าทางสัมผัสย้อนกลับเพื่อออกจากหน้าเว็บได้ แต่จะกลับไปเปิดอีกครั้งโดยไม่ตั้งใจไม่ได้ (เนื่องจากระบบนำรายการออกไปแล้ว) ซึ่งทำไม่ได้ด้วย History API ในปัจจุบัน
ลองใช้ Navigation API
Navigation API พร้อมใช้งานใน Chrome 102 โดยไม่ต้องใช้ Flag นอกจากนี้ คุณยังลองใช้เดโมของ Domenic Denicola ได้ด้วย
แม้ว่า History API แบบคลาสสิกจะดูตรงไปตรงมา แต่ก็ไม่ได้มีการกำหนดไว้อย่างชัดเจนนักและมีปัญหาจำนวนมากเกี่ยวกับกรณีขอบและวิธีที่เบราว์เซอร์ต่างๆ นำไปใช้ เราหวังว่าคุณจะพิจารณาแสดงความคิดเห็นเกี่ยวกับ Navigation API ใหม่
ข้อมูลอ้างอิง
- WICG/navigation-api
- ตำแหน่งมาตรฐานของ Mozilla
- ความตั้งใจที่จะสร้างต้นแบบ
- การตรวจสอบแท็ก
- รายการใน Chromestatus
คำขอบคุณ
ขอขอบคุณ Thomas Steiner, Domenic Denicola และ Nate Chapin ที่ตรวจสอบโพสต์นี้