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

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

瀏覽器支援

  • 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 存取此值。

請參閱這個設定檔示範,瞭解如何在 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 對使用者的影響,例如評估 Core Web Vitals 的影響,否則請避免使用 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 錯誤