סטנדרטיזציה של ניתוב בצד הלקוח באמצעות API חדש לגמרי, שמשנה לחלוטין את האופן שבו בונים אפליקציות של דף יחיד.
אפליקציות בדף יחיד (SPA) מוגדרות על ידי תכונה מרכזית: הן משכתבות את התוכן שלהן באופן דינמי בזמן שהמשתמש מקיים אינטראקציה עם האתר, במקום להשתמש בשיטת ברירת המחדל של טעינת דפים חדשים לגמרי מהשרת.
אמנם אפליקציות SPA הצליחו לספק את התכונה הזו באמצעות History API (או במקרים מוגבלים, באמצעות שינוי החלק #hash של האתר), אבל מדובר ב-API מסורבל שפותח הרבה לפני שאפליקציות SPA הפכו לנורמה – והאינטרנט זקוק לגישה חדשה לחלוטין. Navigation API הוא API מוצע שמבצע שינוי מקיף בתחום הזה, במקום לנסות לתקן את הבעיות ב-History API. (לדוגמה, Scroll Restoration תיקן את History API במקום לנסות להמציא אותו מחדש).
במאמר הזה נסביר על Navigation API ברמה גבוהה. כדי לקרוא את ההצעה הטכנית, אפשר לעיין בטיוטת הדוח במאגר של WICG.
דוגמה לשימוש
כדי להשתמש ב-Navigation API, מתחילים בהוספת מאזין "navigate" לאובייקט navigation הגלובלי.
האירוע הזה הוא מרכזי: הוא יופעל לכל סוגי הניווטים, בין אם המשתמש ביצע פעולה (כמו לחיצה על קישור, שליחת טופס או חזרה קדימה ואחורה) ובין אם הניווט מופעל באופן פרוגרמטי (כלומר, באמצעות הקוד של האתר).
ברוב המקרים, הקוד מאפשר לבטל את התנהגות ברירת המחדל של הדפדפן עבור הפעולה הזו.
ב-SPA, סביר להניח שהמשמעות היא שהמשתמש נשאר באותו דף והתוכן של האתר נטען או משתנה.
אובייקט NavigateEvent מועבר למאזין "navigate", שמכיל מידע על הניווט, כמו כתובת ה-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});
}
});
יש שתי דרכים להתמודד עם הניווט:
- התקשרות אל
intercept({ handler })(כפי שמתואר למעלה) כדי לטפל בניווט. - להתקשר אל
preventDefault(), מה שיכול לבטל את הניווט לגמרי.
בדוגמה הזו, הפונקציה intercept() מופעלת באירוע.
הדפדפן קורא לקריאה החוזרת handler, שצריכה להגדיר את המצב הבא של האתר.
פעולה זו תיצור אובייקט מעבר, navigation.transition, שקוד אחר יכול להשתמש בו כדי לעקוב אחר התקדמות הניווט.
בדרך כלל אפשר להשתמש ב-intercept() וב-preventDefault(), אבל יש מקרים שבהם אי אפשר להשתמש בהם.
אי אפשר לטפל בניווטים באמצעות intercept() אם הניווט הוא ניווט חוצה מקורות.
בנוסף, אי אפשר לבטל ניווט באמצעות preventDefault() אם המשתמש לוחץ על הלחצנים 'הקודם' או 'הבא' בדפדפן. אסור לגרום למשתמשים להיתקע באתר.
(מתנהל דיון בנושא ב-GitHub).
גם אם אי אפשר לעצור או ליירט את הניווט עצמו, האירוע "navigate" עדיין יופעל.
הוא אינפורמטיבי, ולכן הקוד יכול, למשל, לרשום אירוע ב-Analytics כדי לציין שמשתמש עוזב את האתר.
למה כדאי להוסיף עוד אירוע לפלטפורמה?
פונקציית event listener מרכזת את הטיפול בשינויים בכתובות URL בתוך SPA."navigate"
קשה להשיג את זה באמצעות ממשקי 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 יכול לעזור במידה מסוימת עם האפשרויות האלה.
עם זאת, יש לו רק שני אזורים שבהם הוא פועל: תגובה אם המשתמש לוחץ על 'הקודם' או על 'הבא' בדפדפן, וכן שליחה והחלפה של כתובות URL.
אין לו אנלוגיה ל-"navigate", אלא אם מגדירים ידנית מאזינים לאירועי קליק, למשל, כמו שמוצג למעלה.
איך מחליטים איך לטפל בניווט
navigateEvent מכיל הרבה מידע על הניווט, שבעזרתו תוכלו להחליט איך לטפל בניווט מסוים.
המאפיינים העיקריים הם:
canIntercept- אם הערך הוא false, אי אפשר ליירט את הניווט. אי אפשר ליירט ניווטים בין מקורות שונים ומעברים בין מסמכים שונים.
destination.url- זה כנראה המידע הכי חשוב שצריך לקחת בחשבון כשמטפלים בניווט.
hashChange- הערך הוא True אם הניווט הוא באותו מסמך, וה-hash הוא החלק היחיד בכתובת ה-URL ששונה מכתובת ה-URL הנוכחית.
ב-SPA מודרניים, הגיבוב צריך לשמש לקישור לחלקים שונים במסמך הנוכחי. לכן, אם
hashChangeהוא true, כנראה שלא צריך ליירט את הניווט הזה. downloadRequest- אם הערך הוא true, הניווט הופעל על ידי קישור עם מאפיין
download. ברוב המקרים, אין צורך ליירט את הבקשה הזו. formData
- If this isn't null, then this navigation is part of a POST form submission.
חשוב לקחת את זה בחשבון כשמטפלים בניווט.
אם רוצים לטפל רק בניווטים מסוג 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 }) מתוך מאזין "navigate", הוא מודיע לדפדפן שהוא מתכונן להעביר את הדף למצב החדש והמעודכן, ושהניווט עשוי להימשך זמן מה.
הדפדפן מתחיל בלכידת מיקום הגלילה של המצב הנוכחי, כדי שאפשר יהיה לשחזר אותו מאוחר יותר, ואז הוא קורא לפונקציית הקריאה החוזרת 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 ומצב קודמים לכתובת URL ומצב חדשים. יש לכך מספר יתרונות פוטנציאליים, כולל נגישות: דפדפנים יכולים להציג את ההתחלה, הסיום או הכשל הפוטנציאלי של ניווט. לדוגמה, ב-Chrome, מופעל אינדיקטור הטעינה המובנה, והמשתמש יכול ללחוץ על לחצן העצירה. (בשלב הזה, זה לא קורה כשמשתמש עובר בין דפים באמצעות הלחצנים 'הקודם' ו'הבא', אבל הבעיה הזו תתוקן בקרוב).
אישור הניווט
כשמיירטים ניווטים, כתובת ה-URL החדשה תיכנס לתוקף רגע לפני הקריאה החוזרת של handler.
אם לא מעדכנים את ה-DOM באופן מיידי, נוצר פרק זמן שבו התוכן הישן מוצג לצד כתובת ה-URL החדשה.
היא משפיעה על דברים כמו פתרון של כתובות URL יחסיות כשמאחזרים נתונים או כשמטעינים משאבי משנה חדשים.
ב-GitHub מתנהל דיון על דרך לעכב את השינוי בכתובת ה-URL, אבל בדרך כלל מומלץ לעדכן מיד את הדף עם placeholder כלשהו לתוכן הנכנס:
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() handler, יכול להיות שהניווט יהפוך למיותר.
זה קורה במקרים הבאים:
- המשתמש לוחץ על קישור אחר, או שקוד מסוים מבצע ניווט אחר. במקרה כזה, הניווט הישן לא יפעל יותר והניווט החדש יפעל במקומו.
- המשתמש לוחץ על הלחצן 'עצירה' בדפדפן.
כדי להתמודד עם כל אחת מהאפשרויות האלה, האירוע שמועבר אל הפונקציה "navigate" listener מכיל את הנכס signal, שהוא AbortSignal.
מידע נוסף זמין במאמר בנושא Abortable fetch.
בקיצור, הוא מספק אובייקט שמפעיל אירוע כשצריך להפסיק את העבודה.
חשוב לציין שאפשר להעביר 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() {
// …
},
});
אירועי הצלחה וכישלון
כשקוראים ל-handler של intercept(), אחת משתי האפשרויות הבאות תתרחש:
- אם הערך המוחזר של
Promiseהואintercept()(או אם לא קראתם ל-intercept()), Navigation API יפעיל את"navigatesuccess"עםEvent. - אם הערך המוחזר של
Promiseהוא rejects, ה-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" event listener, שמקבל 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;
מדינה (State)
ממשק 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}), ואז מאזין "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. אחרת, עדיף להשתמש באובייקט state.
גישה לכל הרשומות
אבל הנתון 'הערך הנוכחי' לא כולל את כל המידע.
בנוסף, ה-API מספק דרך לגשת לרשימה המלאה של הרשומות שהמשתמש עבר בהן בזמן השימוש באתר באמצעות קריאת navigation.entries(), שמחזירה מערך של רשומות של צילום מצב.
אפשר להשתמש בזה, למשל, כדי להציג ממשק משתמש שונה בהתאם לאופן שבו המשתמש ניווט לדף מסוים, או פשוט כדי לבדוק את כתובות ה-URL הקודמות או את המצבים שלהן.
אי אפשר לעשות את זה באמצעות History API הנוכחי.
אפשר גם להאזין לאירוע "dispose" בפריטי NavigationHistoryEntry בודדים, שמופעל כשהערך כבר לא חלק מהיסטוריית הדפדפן. זה יכול לקרות כחלק מניקוי כללי, אבל גם במהלך ניווט. לדוגמה, אם חוזרים אחורה 10 מקומות ואז עוברים קדימה, 10 הרשומות האלה בהיסטוריה יימחקו.
דוגמאות
האירוע "navigate" מופעל לכל סוגי הניווט, כמו שצוין למעלה.
(יש נספח ארוך במפרט עם כל הסוגים האפשריים).
באתרים רבים, המקרה הנפוץ ביותר הוא שהמשתמש לוחץ על <a href="...">, אבל יש שני סוגים בולטים ומורכבים יותר של ניווט שכדאי להכיר.
ניווט פרוגרמטי
הסוג הראשון הוא ניווט פרוגרמטי, שבו הניווט נגרם על ידי קריאה לשיטה בתוך הקוד בצד הלקוח.
אפשר להתקשר אל navigation.navigate('/another_page') מכל מקום בקוד כדי לגרום לניווט.
הטיפול בזה יתבצע על ידי ה-event listener המרכזי שרשום ב-listener "navigate", וה-listener המרכזי שלכם יופעל באופן סינכרוני.
הכוונה היא לשפר את הצבירה של שיטות ישנות יותר כמו location.assign() ודומות, וגם את השיטות pushState() ו-replaceState() של History API.
השיטה navigation.navigate() מחזירה אובייקט שמכיל שני מופעים של Promise ב-{ committed, finished }.
כך, הפונקציה שקוראת ל-API יכולה להמתין עד שהמעבר יתבצע (כתובת ה-URL הגלויה השתנתה וזמין NavigationHistoryEntry חדש) או יסתיים (כל ההבטחות שהוחזרו על ידי intercept({ handler }) הושלמו – או נדחו, בגלל כשל או בגלל שהמעבר נקטע על ידי ניווט אחר).
ל-method navigate יש גם אובייקט אפשרויות שבו אפשר להגדיר:
-
state: המצב של רשומה חדשה בהיסטוריה, כפי שזמין באמצעות ה-method.getState()ב-NavigationHistoryEntry. -
history: אפשר להגדיר את הערך"replace"כדי להחליף את הרשומה הנוכחית בהיסטוריה. -
info: אובייקט להעברה לאירוע הניווט באמצעותnavigateEvent.info.
לדוגמה, יכול להיות שיהיה שימוש ב-info כדי לציין אנימציה מסוימת שגורמת להצגת הדף הבא.
(אפשרות אחרת היא להגדיר משתנה גלובלי או לכלול אותו כחלק מה-hash#). שתי האפשרויות קצת מסורבלות.)
חשוב לציין שהסרטון הזה info לא יופעל מחדש אם המשתמש יגרום לניווט מאוחר יותר, למשל באמצעות הלחצנים 'הקודם' ו'הבא'.
למעשה, במקרים כאלה הוא תמיד יהיה undefined.
ב-navigation יש גם מספר שיטות ניווט אחרות, שכולן מחזירות אובייקט שמכיל את { committed, finished }.
כבר הזכרתי את traverseTo() (שמקבל key שמציין רשומה ספציפית בהיסטוריה של המשתמש) ואת navigate().
היא כוללת גם את back(), forward() ו-reload().
כל השיטות האלה מטופלות – בדיוק כמו navigate() – על ידי מאזין האירועים המרכזי "navigate".
שליחות של טפסים
שנית, שליחת HTML <form> באמצעות POST היא סוג מיוחד של ניווט, וממשק ה-API לניווט יכול ליירט אותה.
הניווט עדיין מנוהל באופן מרכזי על ידי "navigate"המאזין, למרות שהוא כולל מטען ייעודי (Payload) נוסף.
אפשר לזהות שליחת טופס על ידי חיפוש המאפיין 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>iframe ספציפי אחד.
יש לכך כמה השלכות מעניינות שמפורטות במסמך המפרט, אבל בפועל זה יפחית את הבלבול בקרב המפתחים.
לממשק הקודם History API יש מספר מקרים קיצוניים מבלבלים, כמו תמיכה בפריימים, וממשק Navigation API החדש מטפל במקרים הקיצוניים האלה מההתחלה.
לבסוף, עדיין אין הסכמה לגבי שינוי או סידור מחדש של רשימת הערכים שהמשתמש ניווט דרכם באופן אוטומטי. הנושא הזה נמצא כרגע בדיון, אבל אפשרות אחת היא לאפשר רק מחיקות: או של רשומות היסטוריות או של "כל הרשומות העתידיות". האפשרות השנייה תאפשר מצב זמני. לדוגמה, כמפתח, אוכל:
- לשאול את המשתמש שאלה על ידי מעבר לכתובת URL או למצב חדש
- לאפשר למשתמש להשלים את העבודה (או לחזור אחורה)
- הסרת רשומה בהיסטוריה לאחר השלמת משימה
האפשרות הזו מתאימה במיוחד לחלונות קופצים זמניים או למודעות מעברון: כתובת ה-URL החדשה היא כזו שמשתמש יכול להשתמש בתנועת החלקה 'חזרה' כדי לצאת ממנה, אבל הוא לא יכול בטעות להחליק 'קדימה' כדי לפתוח אותה שוב (כי הרשומה הוסרה). אי אפשר לעשות את זה עם History API הנוכחי.
התנסות עם Navigation API
ה-API של הניווט זמין ב-Chrome 102 ללא תכונות ניסיוניות. אפשר גם לנסות הדגמה של Domenic Denicola.
ממשק ה-API הקלאסי של ההיסטוריה נראה פשוט, אבל הוא לא מוגדר היטב ויש לו מספר גדול של בעיות שקשורות למקרים חריגים ולאופן ההטמעה שלו בדפדפנים שונים. נשמח לקבל ממך משוב על Navigation API החדש.
קובצי עזר
תודות
תודה ל-Thomas Steiner, Domenic Denicola ול-Nate Chapin על בדיקת הפוסט הזה.