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

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

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

  • Chrome: 102.
  • Edge:‏ 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() באירוע. הדפדפן קורא ל-callback של handler, שצריך להגדיר את המצב הבא של האתר. הפעולה הזו תיצור אובייקט מעבר, navigation.transition, שקוד אחר יכול להשתמש בו כדי לעקוב אחרי התקדמות הניווט.

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

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

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

רכיב מעקב אירועים מסוג "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 יכול לעזור באפשרויות האלה. עם זאת, יש לו רק שתי חזיתות: תגובה אם המשתמש לוחץ על 'הקודם' או 'הבא' בדפדפן, וכן דחיפת כתובות URL והחלפתן. אין לו אנלוגיה ל-"navigate", אלא אם מגדירים באופן ידני מאזינים לאירועי קליקים, לדוגמה, כפי שמוצג למעלה.

החלטה איך לטפל באירוע ניווט

השדה navigateEvent מכיל הרבה מידע על הניווט, שבעזרתו תוכלו להחליט איך לטפל בניווט מסוים.

המאפיינים העיקריים הם:

canIntercept
אם הערך הוא false, אי אפשר ליירט את הניווט. אי אפשר ליירט ניווטים בין מקורות שונים ותנועה בין מסמכים שונים.
destination.url
זהו כנראה המידע החשוב ביותר שצריך להביא בחשבון כשמפעילים את הניווט.
hashChange
ערך TRUE אם הניווט הוא באותו מסמך, וה-hash הוא החלק היחיד בכתובת ה-URL ששונה מכתובת ה-URL הנוכחית. ב-SPAs מודרניים, ה-hash צריך לשמש לקישור לחלקים שונים של המסמך הנוכחי. לכן, אם הערך של 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, אלא גם שהתגובה למשתמש נראית מהירה יותר כי היא מתקבלת באופן מיידי.

אותות ביטול

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

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

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

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

כברירת מחדל, זה קורה אחרי שההבטחה שחוזרת מה-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() {
    // …
  },
});

אירועי הצלחה וכישלון

כשמתבצעת קריאה למטפל 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 שימושי כי 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}), ואז המאזין "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="...">, אבל יש שני סוגים בולטים ומורכבים יותר של ניווט שחשוב להתייחס אליהם.

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

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

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

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

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

קובצי עזר

אישורים

תודה ל-Thomas Steiner, ל-Domenic Denicola ול-Nate Chapin על בדיקת הפוסט הזה.