ניתוב מודרני בצד הלקוח: ממשק ה-API לניווט

סטנדרטיזציה של הניתוב בצד הלקוח דרך ממשק API חדש שמשנה באופן מלא את הבנייה של אפליקציות בדף יחיד.

תמיכה בדפדפן

  • Chrome: 102.
  • קצה: 102.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

יישומים של דף יחיד, או שירותי SPA, מוגדרים על ידי תכונה ליבה: שכתוב באופן דינמי את התוכן שלהם בהתאם לאינטראקציה של המשתמש עם האתר, במקום להשתמש בשיטת ברירת המחדל של טעינת דפים חדשים לגמרי מהשרת.

ספקי ספא יכולים כבר עכשיו להשתמש בתכונה הזו דרך ה-History API (או במקרים מוגבלים על ידי שינוי חלק ה-hash של האתר), אבל מדובר בממשק API מגושם שפותח הרבה לפני ש-SPA היה הנורמה – והאינטרנט קורא לגישה חדשה לגמרי. 'API ניווט' הוא ממשק API מוצע שמשנה את כל השטח בתחום הזה, במקום לנסות לתקן את הקצוות הגולמיים של ה-API של ההיסטוריה. (לדוגמה, על ידי Scroll recovery, תיקון של History API במקום לנסות להמציא אותו מחדש.)

פוסט זה מתאר באופן כללי את Navigation API. אם אתם רוצים לקרוא את ההצעה הטכנית, תוכלו לעיין בדוח טיוטת הדוח במאגר של WICG.

שימוש לדוגמה

כדי להשתמש ב-Navigation API, צריך קודם להוסיף אוזן "navigate" באובייקט navigation הגלובלי. האירוע הזה ריכוזי: הוא יופעל בכל סוגי הניווטים, גם אם המשתמש ביצע פעולה (למשל, לחיצה על קישור, שליחת טופס או חזרה קדימה ואחורה) או כשהניווט מופעל באופן פרוגרמטי (כלומר, דרך קוד האתר). ברוב המקרים, הקוד מאפשר לשנות את התנהגות ברירת המחדל של הדפדפן עבור הפעולה הזו. במקרה של שירותי SPA, משמעות הדבר היא שסביר להניח שהמשתמש יישאר באותו דף ולטעון או לשנות את תוכן האתר.

NavigateEvent מועבר ל-listener "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});
  }
});

אפשר לטפל בניווט באחת משתי דרכים:

  • מתבצעת התקשרות אל intercept({ handler }) (כפי שמתואר למעלה) כדי לטפל בניווט.
  • מתבצעת התקשרות למספר preventDefault(). הפעולה הזו עשויה לבטל את הניווט לגמרי.

בדוגמה הזו מתבצעת קריאה לintercept() באירוע. הדפדפן מפעיל את הקריאה החוזרת של handler, שאמורה להגדיר את המצב הבא של האתר. הפעולה הזו תיצור אובייקט מעבר, navigation.transition, שבו קוד אחר יוכל להשתמש כדי לעקוב אחר התקדמות הניווט.

בדרך כלל מותר להשתמש גם ב-intercept() וגם ב-preventDefault(), אבל יש מקרים שבהם לא ניתן להתקשר אליהם. אי אפשר לטפל בניווטים דרך intercept() אם מדובר בניווט ממקורות שונים. בנוסף, לא ניתן לבטל ניווט דרך preventDefault() אם המשתמש לוחץ על הלחצנים 'הקודם' או 'הבא' בדפדפן שלו. אסור לכם "ללכוד" את המשתמשים באתר. (נדון בנושא הזה ב-GitHub).

גם אם אי אפשר לעצור או ליירט את הניווט עצמו, האירוע "navigate" עדיין יופעל. המודל אינפורמטיבי, כך שהקוד יכול, לדוגמה, לתעד אירוע Analytics כדי לציין שמשתמש עוזב את האתר.

למה להוסיף עוד אירוע לפלטפורמה?

הכלי להאזנה לאירועים "navigate" מרכז את הטיפול בשינויים בכתובות URL בתוך SPA. זו הצעה מורכבת, כשמשתמשים בממשקי API ישנים יותר. אם אי פעם כתבתם את הניתוב של ספק השירות (SPA) שלכם באמצעות ה-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
אם זה לא נכון, לא ניתן ליירט את הניווט. לא ניתן ליירט ניווטים בין מקורות ומעברים בין מסמכים.
destination.url
זה המידע הכי חשוב שצריך לקחת בחשבון במהלך הטיפול בניווט.
hashChange
True אם הניווט הוא באותו מסמך, והגיבוב (hash) הוא החלק היחיד בכתובת ה-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 }) מתוך ה-listen "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 ומצב קודמים לכתובת URL חדשה. יש לכך מספר יתרונות פוטנציאליים, כולל נגישות: דפדפנים יכולים להציג את ההתחלה, הסיום או כשל פוטנציאלי של ניווט. Chrome, למשל, מפעיל את אינדיקטור הטעינה המקורי שלו ומאפשר למשתמש ליצור אינטראקציה עם לחצן העצירה. (המצב הזה לא קורה כרגע כשהמשתמש מנווט באמצעות לחצני 'הקודם'/'הבא', אבל המצב הזה תיפתר בקרוב).

בזמן יירוט ניווטים, כתובת ה-URL החדשה תיכנס לתוקף ממש לפני הקריאה החוזרת (callback) של 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, אלא גם ליצור חוויה מהירה כי אתם מגיבים למשתמש באופן מיידי.

ביטול אותות

מכיוון שניתן לבצע עבודה אסינכרונית ב-handler של intercept(), יכול להיות שהניווט יהפוך ליתירות. מצב זה קורה כאשר:

  • המשתמש לוחץ על קישור אחר או שקוד מסוים מבצע ניווט אחר. במקרה כזה, הניווט הישן נטוש לטובת הניווט החדש.
  • המשתמש לוחץ על הלחצן 'עצירה'. בדפדפן.

כדי לטפל בכל אחת מהאפשרויות האלה, האירוע שמועבר למאזינים של "navigate" מכיל מאפיין signal, שהוא AbortSignal. אפשר לקרוא מידע נוסף במאמר אחזור Aborable.

בגרסה הקצרה היא בעצם מספקת אובייקט שמפעיל אירוע כשמפסיקים את העבודה. חשוב לציין שאפשר להעביר 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()), API 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}`);
});

ה-event listener של "navigateerror", שמקבל את הפקודה ErrorEvent, שימושי במיוחד ומובטח שיתקבלו שגיאות מהקוד שלכם שמגדיר דף חדש. אפשר פשוט await fetch() לדעת שאם הרשת לא זמינה, השגיאה תנותב בסופו של דבר אל "navigateerror".

navigation.currentEntry מספקת גישה לרשומה הנוכחית. זהו אובייקט שמתאר את המיקום של המשתמש כרגע. הרשומה הזו כוללת את כתובת ה-URL הנוכחית, מטא-נתונים שיכולים לשמש לזיהוי הרשומה הזו לאורך זמן ואת המצב שסופקו על ידי המפתח.

המטא-נתונים כוללים את key, מאפיין מחרוזת ייחודי של כל רשומה שמייצג את הרשומה הנוכחית ואת המשבצת שלה. המפתח הזה נשאר ללא שינוי גם אם כתובת ה-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;

מדינה

ממשק ה-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="...">, אבל יש שני סוגי ניווט בולטים ומורכבים יותר שכדאי לכסות.

ניווט פרוגרמטי

הראשון הוא ניווט פרוגרמטי. הניווט נגרם על ידי הפעלת method בתוך הקוד בצד הלקוח.

אפשר להתקשר אל navigation.navigate('/another_page') מכל מקום בקוד כדי לגרום לניווט. הוא יטופל על ידי ה-event listener המרכזי שרשום ב-listener של "navigate", וה-listener המרכזי ייקרא באופן סינכרוני.

התכונה הזו מיועדת כצבירה משופרת של שיטות ישנות יותר כמו location.assign() וחברים, בנוסף ל-methods pushState() ו-replaceState() של History API.

ה-method navigation.navigate() מחזירה אובייקט שמכיל שתי מכונות Promise ב-{ committed, finished }. כך ה-invoker יכול להמתין עד שהמעבר יהיה "מחויב" (כתובת ה-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(). כל השיטות האלה מטופלות, בדיוק כמו navigate(), באמצעות ה-event listener המרכזי של "navigate".

שליחת טפסים

שנית, שליחת <form> HTML דרך POST היא סוג מיוחד של ניווט, ו-API לניווט יכול ליירט אותו. הוא כולל מטען ייעודי (payload) נוסף, אבל הניווט עדיין מטופל באופן ריכוזי על ידי המאזינים של "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"
      },
    });
  }
});

מה חסר?

למרות האופי הריכוזי של מעבד האירועים של "navigate", המפרט הנוכחי של Navigation API לא מפעיל את "navigate" בטעינה הראשונה של דף. כשמדובר באתרים שמשתמשים בעיבוד בצד השרת (SSR) בכל המדינות, זה יכול להיות בסדר - השרת יכול להחזיר את המצב הראשוני הנכון, וזו הדרך המהירה ביותר להעביר תוכן למשתמשים. אבל יכול להיות שאתרים שמשתמשים בקוד בצד הלקוח ליצירת הדפים שלהם יצטרכו ליצור פונקציה נוספת כדי לאתחל את הדף.

בחירה מכוונת נוספת של ה-Navigation API היא שהוא פועל רק בתוך מסגרת אחת, כלומר הדף ברמה העליונה, או <iframe> ספציפי אחד. יש לכך כמה השלכות מעניינות שתועדו עוד במפרט, אבל בפועל, יפחיתו את הבלבול אצל המפתחים. בגרסה הקודמת של History API יש כמה מקרי קצה מבלבלים, כמו תמיכה במסגרות, וה-Navigation API בעיצוב חדש מטפל במקרי הקצה האלה מההתחלה.

לבסוף, אין עדיין הסכמה לגבי שינוי או סידור מחדש של רשימת הערכים שהמשתמש ניווט בהם. הנושא הזה נמצא כרגע בדיון, אבל אפשרות אחת היא לאפשר רק מחיקות: ערכים היסטוריים או 'כל הרשומות העתידיות'. זה יאפשר מצב זמני. לדוגמה, כמפתח/ת, אוכל:

  • לשאול את המשתמש שאלה על ידי מעבר לכתובת ה-URL או למצב החדש
  • המשתמש יכול להשלים את העבודה שלו (או לחזור אחורה)
  • להסיר רשומה בהיסטוריה לאחר השלמת משימה

זו יכולה להיות אפשרות מושלמת לשימוש במודעות מעברון או בחלונות זמניים: כתובת ה-URL החדשה היא כתובת שהמשתמש יכול להשתמש בתנועת 'הקודם' כדי לצאת ממנה, אבל הוא לא יכול לעבור בטעות קדימה כדי לפתוח אותה שוב (כי הרשומה הוסרה). הדבר פשוט לא אפשרי בממשק ה-API הנוכחי של היסטוריה.

התנסות עם ממשק ה-API לניווט

ממשק הניווט API זמין בגרסה 102 של Chrome ללא דגלים. תוכלו גם לנסות הדגמה של Domenic Denicola.

הממשק הקלאסי של History API נראה פשוט, אבל הוא לא מוגדר במיוחד ויש בו מספר רב של בעיות שקשורות לקורות גג שונות ואופן ההטמעה שלו באופן שונה בדפדפנים שונים. אנחנו מקווים שתשקול לשלוח משוב על הניווט החדש API.

קובצי עזר

אישורים

תודה לתומס סטיינר, Domenic Denicola ול-Nate Chapin שבדקו את הפוסט הזה. תמונה ראשית (Hero) של Un אימייל, מאת Jeremy Zero.