กำหนดมาตรฐานการกำหนดเส้นทางฝั่งไคลเอ็นต์ผ่าน API ใหม่ล่าสุดซึ่งจะปรับปรุงการสร้างแอปพลิเคชันหน้าเว็บเดียวอย่างสมบูรณ์
แอปพลิเคชันหน้าเดียวหรือ SPA นั้นกำหนดโดยฟีเจอร์หลักๆ ซึ่งก็คือการเขียนเนื้อหาใหม่แบบไดนามิกเมื่อผู้ใช้โต้ตอบกับเว็บไซต์แทนวิธีการเริ่มต้นในการโหลดหน้าใหม่ทั้งหมดจากเซิร์ฟเวอร์
แม้ว่า SPA จะใช้ฟีเจอร์นี้ได้ผ่าน History API (หรือในบางกรณีโดยการปรับส่วน #hash ของเว็บไซต์) แต่ API นี้เป็น API ที่ใช้งานยากซึ่งพัฒนาขึ้นก่อนที่ SPA จะเป็นที่นิยม และเว็บก็ต้องการแนวทางใหม่ทั้งหมด Navigation API เป็น API ที่เสนอซึ่งจะปรับปรุงพื้นที่นี้อย่างสมบูรณ์แทนที่จะพยายามแก้ไขข้อบกพร่องของ History API (เช่น Scroll Restoration ทำการแพตช์ 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 })
จากภายใน "navigate"
Listener ของ intercept({ handler })
โค้ดจะแจ้งให้เบราว์เซอร์ทราบว่าตอนนี้กำลังเตรียมหน้าเว็บสำหรับสถานะใหม่ที่อัปเดตแล้ว และการไปยังส่วนต่างๆ อาจใช้เวลาสักครู่
เบราว์เซอร์จะเริ่มด้วยการบันทึกตําแหน่งการเลื่อนสําหรับสถานะปัจจุบัน เพื่อให้สามารถกู้คืนได้ในภายหลัง (ไม่บังคับ) จากนั้นจะเรียก handler
callback
หาก 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 ใหม่จะมีผลก่อนที่ระบบจะเรียก handler
callback
หากคุณไม่อัปเดต 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() {
// …
},
});
การจัดการโฟกัส
เมื่อ 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'});
จากนั้น 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 ก่อนหน้าหรือสถานะของ 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>
ที่เฉพาะเจาะจงรายการเดียว
การดำเนินการนี้ส่งผลที่น่าสนใจหลายประการที่ระบุไว้ในข้อกำหนดเพิ่มเติม แต่ในทางปฏิบัติแล้วจะลดความสับสนของนักพัฒนาแอป
History API เวอร์ชันเก่ามีกรณีขอบเขตที่ทำให้เกิดความสับสนหลายกรณี เช่น การรองรับเฟรม และ Navigation API เวอร์ชันปรับปรุงใหม่จัดการกรณีขอบเขตเหล่านี้ตั้งแต่เริ่มต้น
สุดท้ายนี้ ยังไม่มีความเห็นพ้องกันเกี่ยวกับการแก้ไขหรือจัดเรียงรายการรายการที่ผู้ใช้ไปยังส่วนต่างๆ โดยใช้โปรแกรม เรื่องนี้อยู่ระหว่างการหารือ แต่ตัวเลือกหนึ่งอาจเป็นการอนุญาตให้ลบได้เฉพาะรายการที่ผ่านมาหรือ "รายการทั้งหมดในอนาคต" ส่วนสถานะชั่วคราวจะอนุญาตสถานะชั่วคราว เช่น ในฐานะนักพัฒนาแอป ฉันสามารถดำเนินการต่อไปนี้
- ถามคำถามผู้ใช้โดยไปที่ URL หรือสถานะใหม่
- อนุญาตให้ผู้ใช้ทำงานให้เสร็จ (หรือกลับไปที่หน้าก่อนหน้า)
- นำรายการประวัติออกเมื่องานเสร็จสมบูรณ์
วิธีนี้เหมาะอย่างยิ่งสําหรับโมดัลหรือโฆษณาคั่นระหว่างหน้าชั่วคราว เนื่องจาก URL ใหม่เป็น URL ที่ผู้ใช้สามารถใช้ท่าทางสัมผัส "กลับ" เพื่อออกจากหน้านั้น แต่จะไม่ทําให้ผู้ใช้กด "ไปข้างหน้า" เพื่อเปิด URL นั้นอีกครั้งโดยไม่ได้ตั้งใจ (เนื่องจากระบบนํารายการออกแล้ว) ซึ่ง History API ปัจจุบันไม่สามารถทำได้
ลองใช้ Navigation API
Navigation API พร้อมใช้งานใน Chrome 102 โดยไม่ต้องใช้ Flag นอกจากนี้ คุณยังลองใช้เดโมโดย Domenic Denicola ได้ด้วย
แม้ว่า History API แบบคลาสสิกจะดูเข้าใจง่าย แต่ก็ไม่ได้มีการกําหนดไว้อย่างชัดเจนและมีปัญหาจํานวนมากเกี่ยวกับกรณีที่พบไม่บ่อยและการใช้งานที่แตกต่างกันในเบราว์เซอร์ต่างๆ เราหวังว่าคุณจะแสดงความคิดเห็นเกี่ยวกับ Navigation API เวอร์ชันใหม่
ข้อมูลอ้างอิง
- WICG/navigation-api
- จุดยืนด้านมาตรฐานของ Mozilla
- Intent To Prototype
- การตรวจสอบแท็ก
- รายการ Chromestatus
ขอขอบคุณ
ขอขอบคุณ Thomas Steiner, Domenic Denicola และ Nate Chapin ที่ตรวจสอบโพสต์นี้