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

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

סאם ת'ורוגו
סאם ת'ורוגו
ג'ייק ארצ'יבלד
ג'ייק ארצ'יבלד

תמיכה בדפדפן

  • 102
  • 102
  • x
  • x

מקור

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

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

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

שימוש לדוגמה

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

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

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

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

מעבד אירועים של "navigate" מרכז את הטיפול בשינויים בכתובות URL בתוך SPA. זאת הצעה קשה לשימוש בממשקי API ישנים יותר. אם אי פעם כתבתם את הניתוב עבור ה-SPA שלכם באמצעות ממשק ה-API של Google History, ייתכן שהוספת את הקוד הבא:

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));

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

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

באופן אישי, פעמים רבות מרגיש ש-History API יכול לעזור לאפשרויות האלה. עם זאת, היא כוללת רק שני אזורי פנים: תגובה אם המשתמש לוחץ על הקודם או 'הבא' בדפדפן שלו, וגם דחיפה והחלפה של כתובות אתרים. אין אנלוגיה ל-"navigate", אלא אם הגדרת פונקציות listener לאירועי לחיצה באופן ידני, לדוגמה, כפי שמתואר למעלה.

החלטתי איך לטפל בניווט

הnavigateEvent מכיל מידע רב על ניווט שניתן להשתמש בו כדי להחליט כיצד לטפל בניווט מסוים.

מאפייני המפתח הם:

canIntercept
אם הערך הוא False, לא ניתן ליירט את הניווט. לא ניתן ליירט ניווטים ממקורות שונים ומעברים בין מסמכים.
destination.url
זה המידע החשוב ביותר שצריך לקחת בחשבון כשמטפלים בניווט.
hashChange
True אם הניווט הוא באותו מסמך, וגיבוב הוא החלק היחיד בכתובת ה-URL ששונה מכתובת ה-URL הנוכחית. באפליקציות SPA מודרניות, הגיבוב צריך להיות מיועד לקישור לחלקים שונים של המסמך הנוכחי. לכן, אם הערך hashChange נכון, סביר להניח שאין צורך ליירט את הניווט הזה.
downloadRequest
אם הערך הוא TRUE, הניווט הופעל על ידי קישור עם מאפיין download. ברוב המקרים אין צורך ליירט אותו.
formData
אם לא הזנת ערך null, הניווט הזה הוא חלק משליחת טופס POST. חשוב לקחת זאת בחשבון במהלך הניווט. אם רוצים לטפל רק בניווטים מסוג 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 }) מתוך ה-listener "navigate" שלו, הוא מודיע לדפדפן שהוא מכין עכשיו את הדף למצב החדש והמעודכן, ושתהליך הניווט יכול להימשך זמן מה.

הדפדפן מתחיל לתעד את מיקום הגלילה של המצב הנוכחי, כך שאפשר יהיה לשחזר אותו מאוחר יותר ולאחר מכן הוא קורא לקריאה חוזרת (callback) של handler. אם הפונקציה handler מחזירה הבטחה (פעולה שמתרחשת באופן אוטומטי עם async functions), ההבטחה הזו מציינת את משך זמן הניווט ואם מצליח.

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

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

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

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

כברירת מחדל, מצב זה מתרחש ברגע שההבטחה שהוחזרה ב-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);
      },
    });
  }
});

לחלופין, כדי לבטל לגמרי את ההסכמה לטיפול בגלילה אוטומטית, אפשר להגדיר את האפשרות intercept() של scroll לערך "manual":

navigateEvent.intercept({
  scroll: 'manual',
  async handler() {
    // …
  },
});

מיקוד

אחרי שההבטחה שהוחזרה על ידי handler תיעלם, הדפדפן ימקד את הרכיב הראשון עם הגדרת המאפיין autofocus, או את הרכיב <body> אם אין רכיבים שכוללים את המאפיין הזה.

ניתן לבטל את ההסכמה לפעולה זו על ידי הגדרת האפשרות focusReset של intercept() כ-"manual":

navigateEvent.intercept({
  focusReset: 'manual',
  async handler() {
    // …
  },
});

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

בעת קריאה ל-handler של intercept(), יקרה אחד משני דברים:

  • אם Promise שהוחזר יקיים (או שלא התקשרת אל intercept()), ממשק ה-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 ישתנה כי הרשומה החדשה תיצור יחידת קיבולת (Slot) חדשה.

למפתח, 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, אבל משתפרת ממנה.

ב-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" listener יכול להחיל את המצב הזה. עם זאת, לפעמים שינוי המצב כבר חל במלואו עד שהקוד שמע עליו, למשל, כאשר המשתמש מחליף רכיב <details> או שהמשתמש משנה את המצב של קלט בטופס. במקרים כאלה, ייתכן שתרצו לעדכן את המצב כך שהשינויים יישמרו באמצעות טעינות מחדש ומעברים. זה אפשרי באמצעות updateCurrentEntry():

navigation.updateCurrentEntry({state: newState});

יש גם אירוע שבו אפשר לקבל מידע על השינוי הזה:

navigation.addEventListener('currententrychange', () => {
  console.log(navigation.currentEntry.getState());
});

אבל אם גיליתם שאתם מגיבים לשינויים במצב ב-"currententrychange", יכול להיות שאתם מפצלים או אפילו משכפלים את הקוד להעברת נתונים בין האירוע "navigate" לבין האירוע "currententrychange", ואילו navigation.reload({state: newState}) מאפשר לטפל בזה במקום אחד.

פרמטרים של מצב לעומת פרמטרים של כתובות אתרים

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

אם היית מצפה שהמצב יישמר כשהמשתמש משתף את כתובת ה-URL עם משתמש אחר, כדאי לשמור אותה בכתובת ה-URL. אחרת, עדיף להשתמש באובייקט המצב.

גישה לכל הרשומות

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

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

דוגמאות

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

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

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

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

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

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

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

ל-method 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) נוסף, הניווט עדיין מטופל באופן מרוכז על ידי ה-listener של "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"
      },
    });
  }
});

מה חסר?

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

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

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

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

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

רוצה לנסות את Navigation API?

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

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

קובצי עזר

אישורים

תודה על הביקורת של תומאס סטיינר, דומניק דניקולה וניט צ'פין. תמונה ראשית (Hero) מ-UnFlood, מאת Jeremy Zero.