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

如果兩個不同文件之間發生檢視畫面轉場,就稱為跨文件檢視畫面轉場。這通常是多頁應用程式 (MPA) 的情況。從 Chrome 126 開始,Chrome 支援跨文件檢視畫面的轉場效果。

瀏覽器支援

  • Chrome:126。
  • Edge:126。
  • Firefox:不支援。
  • Safari 技術預覽:支援。

跨文件檢視畫面轉場與同一個文件的檢視畫面轉場採用相同的構件和原則,這點是經過深思熟慮的結果:

  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-rule 完成這項操作。

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

@view-transition {
  navigation: auto;
}

navigation 描述符設為 auto 後,您將選擇允許在下列 NavigationType 中進行檢視轉場:

  • traverse
  • 如果使用者並未透過瀏覽器 UI 機制啟動,則為 pushreplace

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

如果導覽時間過長 (在 Chrome 中超過四秒),系統會使用 TimeoutError DOMException 略過檢視轉場效果。

跨文件檢視畫面轉場示範

請查看以下使用檢視畫面轉場效果建立Stack Navigator 示範的示範影片。這裡沒有對 document.startViewTransition() 的呼叫,而是透過從一個頁面導覽至另一個頁面,觸發檢視畫面轉場效果。

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

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

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

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

pageswappagereveal 事件

瀏覽器支援

  • Chrome:124。
  • Edge:124。
  • Firefox:不支援。
  • Safari:不支援。

資料來源

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

無論是否即將發生檢視畫面轉場,每個同源跨文件導覽都會觸發這兩個事件。如果兩個頁面之間即將發生檢視轉場效果,您可以在這些事件中使用 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();
    }
  }
}

pageswappagereveal 中的 ViewTransition 物件是兩個不同的物件。它們處理各種承諾的方式也不同:

  • pageswap:隱藏文件後,系統會略過舊的 ViewTransition 物件。發生這種情況時,viewTransition.ready 會拒絕,viewTransition.finished 會解析。
  • pagerevealupdateCallBack 應許已在此時解析。您可以使用 viewTransition.readyviewTransition.finished 承諾。

瀏覽器支援

  • Chrome:123。
  • Edge:123。
  • Firefox:不支援。
  • Safari:不支援。

資料來源

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

舉例來說,在 MPA Stack Navigator 中,所使用的動畫類型會視導覽路徑而定:

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

如要執行這項操作,您需要瞭解即將發生的導覽資訊 (pageswap 的情況),或是剛發生的導覽資訊 (pagereveal 的情況)。

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

在已啟用的網頁上,您可以透過 navigation.activation 存取此物件。在 pageswap 事件中,您可以透過 e.activation 存取此值。

請參閱這個 Profiles 示範,瞭解如何在 pageswappagereveal 事件中使用 NavigationActivation 資訊,針對需要參與檢視畫面轉場的元素設定 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。
  • Edge:124。
  • Firefox:不支援。
  • Safari:不支援。

在某些情況下,您可能需要暫緩網頁的首次算繪,直到新 DOM 中出現特定元素為止。這麼做可避免閃爍,並確保您要轉換的狀態穩定。

<head> 中,使用下列中繼標記,定義一或多個元素 ID,這些 ID 必須在網頁首次轉譯前出現。

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

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

在全面採用轉譯阻斷功能之前,請先瞭解逐步轉譯是網路的基本面向,因此在選擇阻斷轉譯時,請務必謹慎行事。阻擋轉譯的影響必須視個案情況評估。根據預設,除非您能主動評估 blocking=render 對使用者的影響,例如評估 網站體驗核心指標的影響,否則請避免使用 blocking=render


查看跨文件檢視轉場中的轉場類型

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

舉例來說,當您在分頁中前往下一頁或上一頁時,建議您根據序列中前往較高或較低頁面的方向,使用不同的動畫。

如要預先設定這些類型,請在 @view-transition at-rule 中加入類型:

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

由於類型只會套用至有效的 View 轉場效果,因此在 View 轉場效果完成後,系統會自動清除類型。因此,類型可與 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 錯誤