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

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

תמיכה בדפדפנים

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

מקור

אפליקציות בדף יחיד (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 כדי לציין שמשתמש עוזב את האתר.

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

רכיב מעקב אירועים מסוג "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 הוא true, סביר להניח שאין צורך ליירט את הניווט הזה.
downloadRequest
אם הערך הזה נכון, הניווט הופעל על ידי קישור עם מאפיין download. ברוב המקרים אין צורך ליירט את הבקשה הזו.
formData
אם הערך לא null, הפנייה הזו היא חלק משליחת טופס 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 }) מתוך המאזין "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}`);
});

מאזין האירועים "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'});

לאחר מכן, ה-event listener של "navigate" יוכל לזהות את השינוי הזה דרך navigateEvent.destination:

navigation.addEventListener('navigate', navigateEvent => {
  console.log(navigateEvent.destination.getState());
});

עדכון מצב באופן סינכרוני

באופן כללי, עדיף לעדכן את המצב באופן אסינכרוני באמצעות navigation.reload({state: newState}), ואז המאזין "navigate" יוכל להחיל את המצב הזה. עם זאת, לפעמים שינוי המצב כבר הופעל במלואו עד שהקוד מקבל על כך הודעה, למשל כשהמשתמש מפעיל או משבית רכיב <details> או כשהמשתמש משנה את המצב של קלט בטופס. במקרים כאלה, כדאי לעדכן את המצב כדי שהשינויים האלה יישמרו במהלך טעינות מחדש וטרaversals. אפשר לעשות זאת באמצעות 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(), שמחזירה מערך של קובץ snapshot של הרשומות. אפשר להשתמש בנתונים האלה, למשל, כדי להציג ממשק משתמש שונה על סמך האופן שבו המשתמש ניווט לדף מסוים, או פשוט כדי לחזור לכתובות ה-URL הקודמות או למצבים שלהן. אי אפשר לעשות זאת באמצעות History API הנוכחי.

אפשר גם להאזין לאירוע "dispose" ב-NavigationHistoryEntry מסוימים. האירוע הזה מופעל כשהרשומה כבר לא חלק מהיסטוריית הגלישה. זה יכול לקרות כחלק מהניקוי הכללי, אבל הוא יכול לקרות גם במהלך הניווט. לדוגמה, אם חוזרים 10 מקומות אחורה ואז מנווטים קדימה, 10 רשומות ההיסטוריה האלה יוסרו.

דוגמאות

האירוע "navigate" מופעל בכל סוגי הניווט, כפי שצוין למעלה. (יש למעשה נספח ארוך במפרט של כל הסוגים האפשריים).

באתרים רבים, התרחיש הנפוץ ביותר הוא כאשר המשתמש לוחץ על <a href="...">, אבל יש שני סוגים בולטים ומורכבים יותר של ניווט שחשוב להתייחס אליהם.

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

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

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

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

השיטה navigation.navigate() מחזירה אובייקט שמכיל שני מופעי Promise ב-{ committed, finished }. כך מבצע ההפעלה יכול להמתין עד שהמעבר 'יאושר' (כתובת ה-URL הגלויה השתנתה ויש NavigationHistoryEntry חדש זמין) או 'יסתיים' (כל ההתחייבויות שהוחזרו על ידי intercept({ handler }) הושלמו – או נדחו, בגלל כשל או עקב ניווט אחר שהקדים אותו).

השיטה 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".

שליחת טפסים

שנית, שליחת <form> ב-HTML באמצעות POST היא סוג מיוחד של ניווט, ו-Navigation 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 החדשה, אבל הם לא יכולים לעבור בטעות ל'הבא' כדי לפתוח אותה שוב (כי הרשומה הוסרה). אי אפשר לעשות זאת באמצעות History API הנוכחי.

התנסות ב-Navigation API

Navigation API זמין ב-Chrome 102 בלי דגלים. תוכלו גם לנסות הדגמה של Domenic Denicola.

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

קובצי עזר

תודות

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