מעברים בין תצוגות של מסמכים שונים באפליקציות עם מספר דפים

כשמעבר תצוגה מתרחש בין שני מסמכים שונים, הוא נקרא מעבר תצוגה בין מסמכים. המצב הזה נפוץ בדרך כלל באפליקציות עם כמה דפים (MPA). אפשר להשתמש במעברים בין תצוגות במסמכים שונים ב-Chrome מגרסה 126 ואילך.

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

  • Chrome: ‏ 126.
  • Edge: ‏ 126.
  • Firefox: לא נתמך.
  • Safari Technology Preview: יש תמיכה.

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

  1. הדפדפן יוצר קובצי snapshot של אלמנטים שיש להם view-transition-name ייחודי גם בדף הישן וגם בדף החדש.
  2. ה-DOM מתעדכן בזמן שהעיבוד מושעה.
  3. ולבסוף, המעברים מבוססים על אנימציות CSS.

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

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

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

שני התנאים האלה מוסברים בהמשך המאמר.


מעברים בין תצוגות במסמכים שונים מוגבלים לניווטים מאותו מקור

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

המקור של דף הוא שילוב של הסכמה, שם המארח והיציאה שבהם נעשה שימוש, כפי שמתואר באתר web.dev.

דוגמה לכתובת URL שבה מודגשים הסכמה, שם המארח והיציאה. ביחד, הם יוצרים את המקור.
כתובת URL לדוגמה שבה מודגשים הסכימה, שם המארח והיציאה. יחד, הם יוצרים את המקור.

לדוגמה, אפשר להשתמש במעבר בין תצוגות במסמכים שונים כשעוברים מ-developer.chrome.com אל developer.chrome.com/blog, כי מדובר באותו מקור. אי אפשר לבצע את המעבר הזה כשמנווטים מ-developer.chrome.com אל www.chrome.com, כי מדובר במקורות שונים ובאותו אתר.


כדי להשתמש במעברים בין תצוגות במסמכים שונים, צריך להביע הסכמה

כדי לבצע מעבר בין שני מסמכים, צריך להביע הסכמה לכך בשני הדפים המשתתפים. כדי לעשות זאת, משתמשים בכלל @view-transition ב-CSS.

בכלל at-rule‏ @view-transition, מגדירים את המאפיין navigation לערך auto כדי להפעיל מעברים בין תצוגות בניווטים במסמכים שונים מאותו מקור.

@view-transition {
  navigation: auto;
}

הגדרת המאפיין navigation לערך auto מאפשרת מעבר בין תצוגות עבור NavigationType‏:

  • traverse
  • push או replace, אם ההפעלה לא בוצעה על ידי המשתמש באמצעות מנגנונים של ממשק המשתמש בדפדפן.

ניווטים שלא נכללים ב-auto הם, למשל, ניווט באמצעות סרגל הכתובות של כתובת ה-URL או לחיצה על סימנייה, וגם כל סוג של טעינת חוזרת שהמשתמש או התסריט יזמו.

אם הניווט נמשך יותר מדי זמן – יותר מארבע שניות ב-Chrome – מעבר התצוגה ינוข้าม באמצעות TimeoutError DOMException.

הדגמה של מעברים בין תצוגות במסמכים שונים

כדאי לצפות בדמו הבא, שבו נעשה שימוש במעברי תצוגה כדי ליצור דמו של Stack Navigator. אין כאן קריאות ל-document.startViewTransition(), מעברי התצוגה מופעלים על ידי ניווט מדף אחד לדף אחר.

הקלטה של הדגמת Stack Navigator. נדרשת גרסה 126 ואילך של Chrome.

התאמה אישית של מעברים בין תצוגות במסמכים שונים

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

התכונות האלה לא נכללות במפרט של View Transition API עצמו, אבל הן מיועדות לשימוש בשילוב איתו.

האירועים pageswap ו-pagereveal

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

  • Chrome: 124.
  • Edge: ‏ 124.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

כדי לאפשר לכם להתאים אישית מעברים בין תצוגות במסמכים שונים, מפרט ה-HTML כולל שני אירועים חדשים שאפשר להשתמש בהם: pageswap ו-pagereveal.

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

  • האירוע pageswap מופעל לפני שפריימים האחרונים של הדף עוברים עיבוד. אפשר להשתמש באפשרות הזו כדי לבצע שינויים בדף היוצא ברגע האחרון, ממש לפני יצירת קובצי ה-snapshot הישנים.
  • האירוע pagereveal מופעל בדף אחרי שהוא הופעל או הופעל מחדש, אבל לפני ההזדמנות הראשונה לעיבוד. בעזרתו תוכלו להתאים אישית את הדף החדש לפני יצירת קובצי ה-snapshot החדשים.

לדוגמה, אפשר להשתמש באירועים האלה כדי להגדיר או לשנות במהירות ערכים מסוימים של view-transition-name, או להעביר נתונים ממסמך אחד למסמך אחר על ידי כתיבת נתונים מ-sessionStorage וקריאת נתונים ממנו, כדי להתאים אישית את המעבר בין התצוגות לפני שהוא פועל בפועל.

let lastClickX, lastClickY;
document.addEventListener('click', (event) => {
  if (event.target.tagName.toLowerCase() === 'a') return;
  lastClickX = event.clientX;
  lastClickY = event.clientY;
});

// Write position to storage on old page
window.addEventListener('pageswap', (event) => {
  if (event.viewTransition && lastClick) {
    sessionStorage.setItem('lastClickX', lastClickX);
    sessionStorage.setItem('lastClickY', lastClickY);
  }
});

// Read position from storage on new page
window.addEventListener('pagereveal', (event) => {
  if (event.viewTransition) {
    lastClickX = sessionStorage.getItem('lastClickX');
    lastClickY = sessionStorage.getItem('lastClickY');
  }
});

אם רוצים, אפשר לדלג על המעבר בשני האירועים.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    if (goodReasonToSkipTheViewTransition()) {
      e.viewTransition.skipTransition();
    }
  }
}

אובייקט ViewTransition ב-pageswap וב-pagereveal הם שני אובייקטים שונים. הן גם מטפלות בהבטחות שונות באופן שונה:

  • pageswap: אחרי שהמסמך מוסתר, האובייקט הישן של ViewTransition ינוข้าม. במקרה כזה, viewTransition.ready דוחה את הבקשה ו-viewTransition.finished פותר אותה.
  • pagereveal: ההבטחה updateCallBack כבר נפתרה בשלב הזה. אפשר להשתמש בהבטחות viewTransition.ready ו-viewTransition.finished.

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

  • Chrome: ‏ 123.
  • Edge: ‏ 123.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

גם באירועים pageswap וגם באירועים pagereveal, אפשר לבצע פעולה על סמך כתובות ה-URL של הדפים הישנים והחדשים.

לדוגמה, ב-MPA Stack Navigator, סוג האנימציה שבו משתמשים תלוי בנתיב הניווט:

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

כדי לעשות זאת, צריך מידע על הניווט. במקרה של pageswap, הניווט עומד להתרחש, ובמקרה של pagereveal, הוא התרחש זה עתה.

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

בדף מופעל, אפשר לגשת לאובייקט הזה דרך navigation.activation. באירוע pageswap, אפשר לגשת לנתונים האלה דרך e.activation.

כדאי לעיין בהדגמה הזו של Profiles, שבה נעשה שימוש במידע NavigationActivation באירועים pageswap ו-pagereveal כדי להגדיר את ערכי view-transition-name ברכיבים שצריכים להשתתף במעבר התצוגה.

כך לא תצטרכו לקשט מראש כל פריט ברשימה באמצעות view-transition-name. במקום זאת, הטעינה מתבצעת בזמן אמת באמצעות JavaScript, רק ברכיבים שצריכים אותה.

הקלטת הדגמת 'פרופילים'. נדרשת גרסה 126 ואילך של Chrome.

הקוד הוא:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove view-transition-names after snapshots have been taken
      // (this to deal with BFCache)
      await e.viewTransition.finished;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'name';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'avatar';

      // Remove names after snapshots have been taken
      // so that we're ready for the next navigation
      await e.viewTransition.ready;
      document.querySelector(`#${profile} span`).style.viewTransitionName = 'none';
      document.querySelector(`#${profile} img`).style.viewTransitionName = 'none';
    }
  }
});

הקוד גם מנקה אחרי עצמו על ידי הסרת הערכים של view-transition-name אחרי שהמעבר בין התצוגות הופעל. כך הדף מוכן לניווטים עוקבים ויכול גם לטפל בסריקה של ההיסטוריה.

כדי לעזור בכך, אפשר להשתמש בפונקציית השירות הזו שמגדירה את הערכים של view-transition-name באופן זמני.

const setTemporaryViewTransitionNames = async (entries, vtPromise) => {
  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = name;
  }

  await vtPromise;

  for (const [$el, name] of entries) {
    $el.style.viewTransitionName = '';
  }
}

עכשיו אפשר לפשט את הקוד הקודם באופן הבא:

// OLD PAGE LOGIC
window.addEventListener('pageswap', async (e) => {
  if (e.viewTransition) {
    const targetUrl = new URL(e.activation.entry.url);

    // Navigating to a profile page
    if (isProfilePage(targetUrl)) {
      const profile = extractProfileNameFromUrl(targetUrl);

      // Set view-transition-name values on the clicked row
      // Clean up after the page got replaced
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.finished);
    }
  }
});

// NEW PAGE LOGIC
window.addEventListener('pagereveal', async (e) => {
  if (e.viewTransition) {
    const fromURL = new URL(navigation.activation.from.url);
    const currentURL = new URL(navigation.activation.entry.url);

    // Navigating from a profile page back to the homepage
    if (isProfilePage(fromURL) && isHomePage(currentURL)) {
      const profile = extractProfileNameFromUrl(currentURL);

      // Set view-transition-name values on the elements in the list
      // Clean up after the snapshots have been taken
      setTemporaryViewTransitionNames([
        [document.querySelector(`#${profile} span`), 'name'],
        [document.querySelector(`#${profile} img`), 'avatar'],
      ], e.viewTransition.ready);
    }
  }
});

המתנה לטעינת התוכן באמצעות חסימה של עיבוד

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

  • Chrome: 124.
  • Edge: ‏ 124.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

במקרים מסוימים, כדאי לדחות את העיבוד הראשון של דף עד שרכיב מסוים יופיע ב-DOM החדש. כך אפשר למנוע הבהוב ולוודא שהמצב שאליו מתבצעת האנימציה יציב.

ב-<head>, מגדירים מזהה רכיב אחד או יותר שצריכים להיות נוכחים לפני שהדף יעבור את העיבוד הראשון, באמצעות המטא תג הבא.

<link rel="expect" blocking="render" href="#section1">

מטא התג הזה מציין שהרכיב צריך להופיע ב-DOM, ולא שהתוכן צריך להיטען. לדוגמה, בתמונות, נוכחות התג <img> עם הערך שצוין של id בעץ ה-DOM מספיקה כדי שהתנאי יהיה נכון. יכול להיות שהתמונה עצמה עדיין נטענת.

לפני שמשתמשים באופן מלא בחסימת עיבוד, חשוב לזכור שעיבוד מצטבר הוא היבט בסיסי של האינטרנט, ולכן צריך להפעיל שיקול דעת כשמחליטים לחסום את העיבוד. צריך להעריך את ההשפעה של חסימה של עיבוד (render) על בסיס כל מקרה לגופו. כברירת מחדל, מומלץ להימנע משימוש ב-blocking=render אלא אם יש לכם אפשרות למדוד ולבחון באופן פעיל את ההשפעה שלו על המשתמשים, על ידי מדידת ההשפעה על המדדים הבסיסיים של חוויית המשתמש (Core Web Vitals).


הצגת סוגי המעברים במעברים בין מסמכים

מעברים בין תצוגות במסמכים שונים תומכים גם בסוגים של מעברים בין תצוגות, כדי להתאים אישית את האנימציות ואת הרכיבים שייכללו בצילום.

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

כדי להגדיר את הסוגים האלה מראש, מוסיפים את הסוגים בכלל @view-transition:

@view-transition {
  navigation: auto;
  types: slide, forwards;
}

כדי להגדיר את הסוגים בזמן אמת, משתמשים באירועים pageswap ו-pagereveal כדי לשנות את הערך של e.viewTransition.types.

window.addEventListener("pagereveal", async (e) => {
  if (e.viewTransition) {
    const transitionType = determineTransitionType(navigation.activation.from, navigation.activation.entry);
    e.viewTransition.types.add(transitionType);
  }
});

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

כדי להגיב לסוגי המעבר האלה, משתמשים בבורר של פסאודו-הקלאס :active-view-transition-type() באותו אופן שבו משתמשים בו במעברים בין תצוגות באותו מסמך.

/* Determine what gets captured when the type is forwards or backwards */
html:active-view-transition-type(forwards, backwards) {
  :root {
    view-transition-name: none;
  }
  article {
    view-transition-name: content;
  }
  .pagination {
    view-transition-name: pagination;
  }
}

/* Animation styles for forwards type only */
html:active-view-transition-type(forwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-left;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-right;
  }
}

/* Animation styles for backwards type only */
html:active-view-transition-type(backwards) {
  &::view-transition-old(content) {
    animation-name: slide-out-to-right;
  }
  &::view-transition-new(content) {
    animation-name: slide-in-from-left;
  }
}

/* Animation styles for reload type only */
html:active-view-transition-type(reload) {
  &::view-transition-old(root) {
    animation-name: fade-out, scale-down;
  }
  &::view-transition-new(root) {
    animation-delay: 0.25s;
    animation-name: fade-in, scale-up;
  }
}

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

הדגמה (דמו)

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

הקלטה של הדגמה של חלוקה לדפים (AMP). המערכת משתמשת במעברים שונים בהתאם לדף שאליו עוברים.

סוג המעבר שבו משתמשים נקבע באירועים pagereveal ו-pageswap על סמך כתובות ה-URL 'אל' ו'מ'.

const determineTransitionType = (fromNavigationEntry, toNavigationEntry) => {
  const currentURL = new URL(fromNavigationEntry.url);
  const destinationURL = new URL(toNavigationEntry.url);

  const currentPathname = currentURL.pathname;
  const destinationPathname = destinationURL.pathname;

  if (currentPathname === destinationPathname) {
    return "reload";
  } else {
    const currentPageIndex = extractPageIndexFromPath(currentPathname);
    const destinationPageIndex = extractPageIndexFromPath(destinationPathname);

    if (currentPageIndex > destinationPageIndex) {
      return 'backwards';
    }
    if (currentPageIndex < destinationPageIndex) {
      return 'forwards';
    }

    return 'unknown';
  }
};

משוב

אנחנו תמיד שמחים לקבל משוב ממפתחים. כדי לשתף, שולחים דיווח על בעיה לקבוצת העבודה של שירותי ה-CSS ב-GitHub עם הצעות ושאלות. מוסיפים את הקידומת [css-view-transitions] לבעיה. אם נתקלתם בבאג, תוכלו לדווח על באג ב-Chromium במקום זאת.