انتقال نمای بین اسناد برای برنامه های کاربردی چند صفحه ای

وقتی یک انتقال نمایش بین دو سند مختلف رخ می‌دهد، به آن انتقال نمایش بین سندی می‌گویند. این معمولاً در برنامه‌های چند صفحه‌ای (MPA) اتفاق می‌افتد. انتقال نمایش بین سندی از نسخه ۱۲۶ کروم در کروم پشتیبانی می‌شود.

Browser Support

  • کروم: ۱۲۶.
  • لبه: ۱۲۶.
  • فایرفاکس: پشتیبانی نمی‌شود.
  • سافاری: ۱۸.۲.

Source

انتقال‌های نمای بین اسنادی (Cross-document view transitions) بر همان بلوک‌های سازنده و اصول انتقال‌های نمای بین اسنادی مشابه (same-document view transitions) متکی هستند، که کاملاً عمدی است:

  1. مرورگر از عناصری که دارای view-transition-name منحصر به فرد در هر دو صفحه قدیمی و جدید هستند، snapshot می‌گیرد.
  2. DOM به‌روزرسانی می‌شود در حالی که رندر متوقف می‌شود.
  3. و در نهایت، انتقال‌ها توسط انیمیشن‌های CSS پشتیبانی می‌شوند.

چیزی که در مقایسه با انتقال‌های نمای سند یکسان متفاوت است، این است که در انتقال‌های نمای سند متقابل، نیازی به فراخوانی document.startViewTransition برای شروع انتقال نما ندارید. در عوض، عامل شروع انتقال نمای سند متقابل، یک ناوبری با مبدأ یکسان از یک صفحه به صفحه دیگر است، عملی که معمولاً توسط کاربر وب‌سایت شما با کلیک روی یک لینک انجام می‌شود.

به عبارت دیگر، هیچ API برای فراخوانی جهت شروع انتقال نما بین دو سند وجود ندارد. با این حال، دو شرط وجود دارد که باید برآورده شوند:

  • هر دو سند باید از یک مبدا باشند.
  • برای اینکه امکان تغییر نما فراهم شود، هر دو صفحه باید این گزینه را فعال کنند.

هر دوی این شرایط بعداً در این سند توضیح داده شده‌اند.


انتقال‌های نمای بین اسنادی به ناوبری‌های با منشأ یکسان محدود می‌شوند

انتقال بین اسناد فقط به ناوبری‌های با مبدا یکسان محدود می‌شود. یک ناوبری در صورتی که مبدا هر دو صفحه شرکت‌کننده یکسان باشد، ناوبری با مبدا یکسان در نظر گرفته می‌شود.

منشأ یک صفحه ترکیبی از طرح استفاده شده، نام میزبان و پورت است، همانطور که در web.dev به تفصیل شرح داده شده است .

یک نمونه URL با طرح، نام میزبان و پورت هایلایت شده. این موارد در کنار هم، مبدا را تشکیل می‌دهند.
یک نمونه URL با طرح، نام میزبان و پورت هایلایت شده. این موارد در کنار هم، مبدا را تشکیل می‌دهند.

برای مثال، می‌توانید هنگام پیمایش از developer.chrome.com به developer.chrome.com/blog ، یک گذار بین سندی داشته باشید، زیرا این دو آدرس از نوع same-origin هستند. اما هنگام پیمایش از developer.chrome.com به www.chrome.com نمی‌توانید چنین گذارهایی داشته باشید، زیرا این دو آدرس از نوع cross-origin و same-site هستند.


انتقال نمای بین اسناد اختیاری است

برای داشتن یک گذار بین دو سند، هر دو صفحه‌ی شرکت‌کننده باید این امکان را بپذیرند. این کار با استفاده از @view-transition at-rule در CSS انجام می‌شود.

در @view-transition at-rule، توصیفگر navigation را روی auto تنظیم کنید تا انتقال نماها برای ناوبری‌های بین سندی و با منشأ یکسان فعال شود.

@view-transition {
  navigation: auto;
}

با تنظیم توصیفگر navigation روی auto شما اجازه می‌دهید که انتقال نماها برای NavigationType های زیر اتفاق بیفتد:

  • traverse
  • اگر فعال‌سازی توسط کاربر و از طریق مکانیسم‌های رابط کاربری مرورگر آغاز نشده باشد، push یا replace .

ناوبری‌هایی که از auto خارج می‌شوند، برای مثال عبارتند از پیمایش با استفاده از نوار آدرس URL یا کلیک روی یک نشانک، و همچنین هر نوع بارگذاری مجدد که توسط کاربر یا اسکریپت آغاز شود.

اگر یک پیمایش خیلی طول بکشد - بیش از چهار ثانیه در مورد کروم - آنگاه انتقال نما با خطای TimeoutError DOMException رد می‌شود.

نسخه نمایشی انتقال بین اسناد

به دموی زیر که از انتقال‌های نما برای ایجاد یک دموی Stack Navigator استفاده می‌کند، نگاهی بیندازید. در اینجا هیچ فراخوانی برای document.startViewTransition() وجود ندارد، انتقال‌های نما با پیمایش از یک صفحه به صفحه دیگر آغاز می‌شوند.

ضبط نسخه آزمایشی Stack Navigator . به کروم ۱۲۶+ نیاز دارد.

سفارشی‌سازی انتقال‌های نمای بین اسنادی

برای سفارشی‌سازی انتقال‌های نمای بین اسناد، برخی از ویژگی‌های پلتفرم وب وجود دارد که می‌توانید از آنها استفاده کنید.

این ویژگی‌ها بخشی از مشخصات خودِ View Transition API نیستند، بلکه طوری طراحی شده‌اند که در ارتباط با آن مورد استفاده قرار گیرند.

رویدادهای pageswap و pagereveal

Browser Support

  • کروم: ۱۲۴.
  • لبه: ۱۲۴.
  • فایرفاکس: پشتیبانی نمی‌شود.
  • سافاری: ۱۸.۲.

Source

برای اینکه بتوانید انتقال‌های نمای بین سندی را سفارشی کنید، مشخصات 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 دو شیء متفاوت هستند. آنها همچنین promise های مختلف را به طور متفاوتی مدیریت می کنند:

  • pageswap : به محض اینکه سند پنهان شود، شیء ViewTransition قدیمی نادیده گرفته می‌شود. وقتی این اتفاق می‌افتد، viewTransition.ready رد می‌شود و viewTransition.finished حل می‌شود.
  • pagereveal : در این مرحله، promise updateCallBack از قبل حل شده است. می‌توانید از promiseهای viewTransition.ready و viewTransition.finished استفاده کنید.

Browser Support

  • کروم: ۱۲۳.
  • لبه: ۱۲۳.
  • فایرفاکس: ۱۴۷.
  • سافاری: ۲۶.۲.

Source

در هر دو رویداد pageswap و pagereveal ، می‌توانید بر اساس URLهای صفحات قدیمی و جدید نیز اقداماتی انجام دهید.

برای مثال، در MPA Stack Navigator نوع انیمیشن مورد استفاده به مسیر ناوبری بستگی دارد:

  • هنگام پیمایش از صفحه نمای کلی به صفحه جزئیات، محتوای جدید باید از راست به چپ حرکت کند.
  • هنگام پیمایش از صفحه جزئیات به صفحه نمای کلی، محتوای قدیمی باید از چپ به راست حرکت کند.

برای انجام این کار، به اطلاعاتی در مورد ناوبری نیاز دارید که در مورد pageswap ، در شرف وقوع است یا در مورد pagereveal تازه اتفاق افتاده است.

برای این منظور، مرورگرها اکنون می‌توانند اشیاء NavigationActivation را که اطلاعاتی در مورد ناوبری با همان مبدا دارند، نمایش دهند. این شیء نوع ناوبری استفاده شده، ورودی‌های تاریخچه فعلی و مقصد نهایی را همانطور که در navigation.entries() از API ناوبری یافت می‌شود، نمایش می‌دهد.

در یک صفحه فعال، می‌توانید از طریق navigation.activation به این شیء دسترسی پیدا کنید. در رویداد pageswap ، می‌توانید از طریق e.activation به آن دسترسی پیدا کنید.

این دموی Profiles را ببینید که از اطلاعات NavigationActivation در رویدادهای pageswap و pagereveal برای تنظیم مقادیر view-transition-name روی عناصری که باید در گذار view شرکت کنند، استفاده می‌کند.

به این ترتیب، لازم نیست تک تک موارد موجود در لیست را از قبل با یک view-transition-name تزئین کنید. در عوض، این اتفاق به صورت خودکار و فقط روی عناصری که به آن نیاز دارند، با استفاده از جاوا اسکریپت رخ می‌دهد.

ضبط دموی پروفایل‌ها . به کروم ۱۲۶+ نیاز دارد.

کد به شرح زیر است:

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

منتظر بمانید تا محتوا با مسدود کردن رندر بارگذاری شود

Browser Support

  • کروم: ۱۲۴.
  • لبه: ۱۲۴.
  • فایرفاکس: پشتیبانی نمی‌شود.
  • سافاری: پشتیبانی نمی‌شود.

در برخی موارد، ممکن است بخواهید اولین رندر یک صفحه را تا زمانی که یک عنصر خاص در DOM جدید وجود داشته باشد، به تعویق بیندازید. این کار از چشمک زدن جلوگیری می‌کند و تضمین می‌کند که حالتی که انیمیشن را روی آن اجرا می‌کنید، پایدار است.

در <head> ، با استفاده از متا تگ زیر، یک یا چند شناسه عنصر را که باید قبل از اولین رندر صفحه وجود داشته باشند، تعریف کنید.

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

این متا تگ به این معنی است که عنصر باید در DOM وجود داشته باشد، نه اینکه محتوا باید بارگذاری شود. برای مثال، در مورد تصاویر، صرفاً وجود تگ <img> با id مشخص شده در درخت DOM برای ارزیابی شرط به صورت درست کافی است. خود تصویر می‌تواند هنوز در حال بارگذاری باشد.

قبل از اینکه کاملاً روی مسدود کردن رندر کار کنید، توجه داشته باشید که رندر افزایشی یک جنبه اساسی وب است، بنابراین هنگام انتخاب مسدود کردن رندر محتاط باشید. تأثیر مسدود کردن رندر باید به صورت موردی ارزیابی شود. به طور پیش‌فرض، از استفاده blocking=render خودداری کنید، مگر اینکه بتوانید با اندازه‌گیری تأثیر آن بر Core Web Vitals ، تأثیر آن را بر کاربران خود به طور فعال اندازه‌گیری و ارزیابی کنید.


مشاهده انواع گذار در گذارهای نمای بین سندی

انتقال‌های نمای بین اسنادی همچنین از انواع انتقال نما برای سفارشی‌سازی انیمیشن‌ها و اینکه کدام عناصر ثبت شوند، پشتیبانی می‌کنند.

برای مثال، هنگام رفتن به صفحه بعدی یا قبلی در یک صفحه‌بندی، ممکن است بخواهید از انیمیشن‌های متفاوتی بسته به اینکه آیا به صفحه بالاتر یا پایین‌تر از دنباله می‌روید، استفاده کنید.

برای تنظیم این نوع‌ها از ابتدا، انواع را در @view-transition at-rule‎ اضافه کنید:

@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 با نگاه کردن به آدرس‌های اینترنتی to و from تعیین می‌شود.

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 ثبت کنید .