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

如果兩個不同文件之間的檢視模式轉換,稱為「跨文件檢視轉換」。多頁面應用程式 (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 規則來完成。

@view-transition 規則中,將 navigation 描述元設為 auto,即可啟用跨文件、相同來源導覽的檢視畫面轉換功能。

@view-transition {
  navigation: auto;
}

navigation 描述元設為 auto,即表示您選擇允許下列 NavigationType 的檢視區塊轉場:

  • traverse
  • pushreplace (如果啟用程序並非由使用者透過瀏覽器 UI 機制啟動)。

auto 排除的瀏覽行為 (例如使用網址列或點選書籤進行導覽),以及任何形式的使用者或指令碼會重新載入。

如果瀏覽時花費的時間太長 (超過 Chrome 的情況) 超過四秒,系統就會使用 TimeoutError DOMException 略過檢視轉場效果。

跨文件檢視轉換示範

請參閱下列示範,瞭解如何使用檢視畫面轉場功能建立堆疊導覽器示範。這裡沒有對 document.startViewTransition() 的呼叫,因此從一個頁面前往另一個頁面時會觸發觀看轉換。

堆疊導覽器示範錄製過程。需要 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 堆疊導覽器中,要使用的動畫類型視導覽路徑而定:

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

如要執行此動作,您需要取得以下導航資訊:在 pageswap 的情況下,或發生在 pagereveal 的情況下才剛發生。

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

在啟用的頁面中,您可以透過 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">

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

全面封鎖轉譯功能之前,請留意漸進式轉譯是網路的一大要素,因此選擇封鎖顯示功能時請格外小心。請依個案評估封鎖轉譯功能的影響。根據預設,除非您可積極評估及衡量 Core Web Vitals 的影響,否則請避免使用 blocking=render


查看跨文件檢視轉場效果的轉換類型

跨文件檢視轉換也支援檢視畫面轉換類型,可自訂動畫及擷取的元素。

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

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

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

由於類型只會套用至 Active 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 錯誤