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

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

תמיכה בדפדפן

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

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

  1. הדפדפן מצלם תמונות מצב של רכיבים בעלי 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.

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

@view-transition {
  navigation: auto;
}

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

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

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

אם הניווט נמשך יותר מ-4 שניות במקרה של דפדפן Chrome, המערכת תדלג על המעבר בין התצוגה באמצעות TimeoutError DOMException.

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

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

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

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

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

התכונות האלה לא חלק מהמפרט של View Transition API, אבל אפשר להשתמש בהן בשילוב עם הממשק הזה.

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

תמיכה בדפדפן

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

מקור

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

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

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

לדוגמה, אפשר להשתמש באירועים האלה כדי להגדיר או לשנות במהירות ערכי 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.
  • קצה: 123.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

מקור

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

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

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

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

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

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

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

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

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

הקוד נראה כך:

// 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.
  • קצה: 124.
  • Firefox: לא נתמך.
  • Safari: לא נתמך.

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

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

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

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

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


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

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

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

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

@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.

הדגמה (דמו)

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

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

סוג המעבר שבו צריך להשתמש נקבע באירועים 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 במקום זאת.