多頁面應用程式的跨文件檢視模式轉換

如果兩個不同的文件之間發生檢視區塊轉場效果,則稱為跨文件檢視區塊轉場效果。多頁面應用程式 (MPA) 通常會發生這種情況。Chrome 126 以上版本支援跨文件檢視畫面轉場效果。

Browser Support

  • Chrome: 126.
  • Edge: 126.
  • Firefox: not supported.
  • Safari: 18.2.

Source

跨文件檢視區塊轉換所依據的建構區塊和原則,與同文件檢視區塊轉換完全相同,這是刻意設計:

  1. 瀏覽器會擷取新舊網頁中具有專屬 view-transition-name 的元素快照。
  2. 系統會更新 DOM,但會抑制算繪作業。
  3. 最後,轉場效果是由 CSS 動畫提供支援。

與同文件檢視區塊轉換相比,跨文件檢視區塊轉換的不同之處在於,您不需要呼叫 document.startViewTransition 即可啟動檢視區塊轉換。跨文件檢視區塊轉換的觸發條件,是從一個網頁導覽至另一個網頁的同源導覽,這項動作通常是由網站使用者點選連結所執行。

換句話說,您無法呼叫 API,在兩份文件之間啟動檢視區塊轉場效果。不過,必須符合下列兩項條件:

  • 兩份文件必須位於相同來源。
  • 兩個頁面都必須選擇啟用,才能允許檢視區塊轉場效果。

本文稍後會說明這兩項條件。


跨文件檢視畫面轉場效果僅限於相同來源的導覽

跨文件檢視畫面轉場效果僅限於同源導覽。如果參與導覽的兩個頁面來源相同,系統就會將導覽視為同源導覽。

網頁來源是所用配置、主機名稱和通訊埠的組合,詳情請參閱 web.dev

網址範例,其中醒目顯示了通訊協定、主機名稱和通訊埠。兩者合起來就是來源。
以下是網址範例,其中醒目顯示配置、主機名稱和通訊埠。這些元素合起來就是來源。

舉例來說,從 developer.chrome.com 導覽至 developer.chrome.com/blog 時,由於兩者屬於相同來源,因此可以進行跨文件檢視區塊轉場效果。從 developer.chrome.com 導覽至 www.chrome.com 時,您無法進行該轉換,因為這些是跨來源和同網站。


跨文件檢視轉場效果為選擇啟用功能

如要在兩個文件之間進行跨文件檢視畫面轉場效果,參與的頁面都必須選擇允許。這項作業是透過 CSS 中的 @view-transition at 規則完成。

@view-transition at 規則中,將 navigation 描述元設為 auto,即可為跨文件、相同來源的導覽啟用檢視區塊轉場效果。

@view-transition {
  navigation: auto;
}

navigation 描述元設為 auto,即表示您選擇允許下列 NavigationType 發生檢視畫面轉場效果:

  • traverse
  • replace,如果啟用不是使用者透過瀏覽器 UI 機制啟動。push

auto 排除的導覽包括使用網址列導覽、點選書籤,以及任何形式的使用者或指令啟動重新載入。

如果導覽時間過長 (以 Chrome 來說,超過四秒),系統就會略過檢視區塊轉換,並顯示 TimeoutError DOMException

跨文件檢視畫面轉場效果的示範

請參閱下列示範,瞭解如何使用檢視區塊轉場效果建立 Stack Navigator 示範。這裡沒有對 document.startViewTransition() 的呼叫,檢視區塊轉場效果是透過從一個頁面導覽至另一個頁面觸發。

Stack Navigator 示範的錄影畫面。需要 Chrome 126 以上版本。

自訂跨文件檢視畫面轉場效果

如要自訂跨文件檢視畫面轉場效果,可以使用一些網頁平台功能。

這些功能本身並非 View Transition API 規格的一部分,但設計上可與該規格搭配使用。

pageswap」和「pagereveal」事件

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: not supported.
  • Safari: 18.2.

Source

為方便您自訂跨文件檢視畫面轉場效果,HTML 規格包含兩個可用的新事件:pageswappagereveal

無論是否即將發生檢視區塊轉場效果,這兩個事件都會針對每個同源跨文件導覽觸發。如果兩個網頁之間即將發生檢視區塊轉換,您可以使用這些事件的 viewTransition 屬性存取 ViewTransition 物件。

  • pageswap 事件會在網頁的最後一個影格算繪前觸發。您可以在擷取舊版快照前,使用這項功能對即將離開的網頁進行最後的變更。
  • 網頁初始化或重新啟動後,在第一次算繪機會前,會觸發 pagereveal 事件。你可以先自訂新頁面,再拍攝新的快照。

舉例來說,您可以透過這些事件,從 sessionStorage 寫入及讀取資料,快速設定或變更部分 view-transition-name 值,或將資料從一個文件傳遞至另一個文件,在實際執行檢視區塊轉場效果進行自訂。

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

pageswappagereveal 中的 ViewTransition 物件是兩個不同的物件。此外,這兩種方法處理各種 Promise 的方式也不同:

  • pageswap:文件隱藏後,系統會略過舊的 ViewTransition 物件。這時,viewTransition.ready 會拒絕,viewTransition.finished 則會解決。
  • pagereveal:此時 updateCallBack promise 已解決。您可以使用 viewTransition.readyviewTransition.finished 承諾。

Browser Support

  • Chrome: 123.
  • Edge: 123.
  • Firefox: 147.
  • Safari: 26.2.

Source

pageswappagereveal 事件中,您也可以根據新舊網頁的網址採取行動。

舉例來說,在 MPA Stack Navigator 中,要使用的動畫類型取決於導覽路徑:

  • 從總覽頁面導覽至詳細資料頁面時,新內容必須從右向左滑入。
  • 從詳細資料頁面導覽至總覽頁面時,舊內容必須從左向右滑出。

如要執行這項操作,您需要導覽相關資訊。以 pageswap 來說,這類資訊與即將發生的導覽有關;以 pagereveal 來說,則與剛發生的導覽有關。

為此,瀏覽器現在可以公開 NavigationActivation 物件,其中包含同源導覽的相關資訊。這個物件會公開所用的導覽類型、目前和最終目的地記錄項目 (如 navigation.entries() 中的 Navigation API 所示)。

在已啟用的頁面上,您可以透過 navigation.activation 存取這個物件。在 pageswap 事件中,您可以透過 e.activation 存取這項資訊。

請參閱這個設定檔示範,瞭解如何使用 NavigationActivation 資訊,在 pageswappagereveal 事件中設定需要參與檢視區塊轉換的元素 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);
    }
  }
});

等待內容載入 (會造成轉譯遭到封鎖)

Browser Support

  • Chrome: 124.
  • Edge: 124.
  • Firefox: not supported.
  • Safari: not supported.

在某些情況下,您可能想延後網頁的首次算繪,直到新 DOM 中出現特定元素為止。這樣可避免閃爍,並確保動畫狀態穩定。

<head> 中,使用下列中繼標記定義一或多個元素 ID,這些 ID 必須存在,網頁才能進行首次算繪。

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

這個中繼標記表示元素應存在於 DOM 中,而非內容應載入。舉例來說,如果是圖片,只要 DOM 樹狀結構中存在含有指定 id<img> 標記,條件評估結果就會是 True。圖片本身可能仍在載入中。

請注意,在全力避免算繪遭到封鎖之前,請先瞭解遞增式算繪是網頁的基本要素,因此選擇封鎖算繪時請務必謹慎。封鎖算繪的影響應視個案情況評估。除非您能主動評估 blocking=render 對使用者的影響,並透過評估網站體驗核心指標來衡量影響程度,否則請避免使用這項功能。


在跨文件檢視畫面轉場效果中查看轉場類型

跨文件檢視區塊轉場效果也支援檢視區塊轉場效果類型,可自訂動畫和要擷取的元素。

舉例來說,在分頁中前往下一頁或上一頁時,您可能會想根據要前往序列中的較高或較低頁面,使用不同的動畫。

如要預先設定這些類型,請在 @view-transition at 規則中新增類型:

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

如要即時設定型別,請使用 pageswappagereveal 事件來操控 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) 的錄製內容。系統會根據你要前往的頁面使用不同的轉場效果。

系統會查看來源和目標網址,在 pagerevealpageswap 事件中判斷要使用的轉換類型。

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';
  }
};

意見回饋

我們非常重視開發人員的意見。如要分享,請在 GitHub 向 CSS 工作小組回報問題,並提供建議和問題。請在問題前加上 [css-view-transitions]。 如果遇到錯誤,請改為回報 Chromium 錯誤