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

不同文件之間的檢視畫面轉換時,稱為「跨文件檢視轉換」。這在多頁應用程式 (MPA) 中通常都是如此。在 Chrome 126 版中,Chrome 支援跨文件檢視轉場功能。

瀏覽器支援

  • 126
  • 126
  • x
  • x

來源

跨文件檢視轉換功能仰賴的建構模塊和原則,與同一份文件的檢視畫面轉換效果相同,但以下幾點:

  1. 瀏覽器會針對新舊網頁中含有專屬 view-transition-name 的元素拍攝快照。
  2. DOM 會在算繪暫停時更新。
  3. 最後,轉場效果是由 CSS 動畫驅動。

與相同文件檢視模式轉換相比,不同之處在於:在使用跨文件檢視轉場效果時,不必呼叫 document.startViewTransition 就能啟動檢視畫面轉場。相反地,跨文件檢視轉換的觸發事件是從一個頁面到另一個頁面進行導覽,而動作通常是由網站訪客點選連結所執行。

也就是說,沒有 API 可以呼叫,以便在兩份文件之間開始檢視轉換。不過,你必須具備以下兩個條件:

  • 兩份文件必須位於相同的來源中。
  • 這兩個頁面都必須選擇啟用才能轉換檢視畫面。

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


跨文件檢視轉換功能只適用於相同來源瀏覽

跨文件檢視轉換功能僅適用於相同來源瀏覽。如果兩個參與網頁的來源相同,系統會將導覽視為相同來源。

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

醒目顯示配置、主機名稱和通訊埠的範例網址。兩者結合後,就會形成來源。
已醒目顯示配置、主機名稱和通訊埠的範例網址。兩者結合後,就會形成來源。

舉例來說,從 developer.chrome.com 瀏覽到 developer.chrome.com/blog 時,您可以執行跨文件檢視轉換,因為兩者屬於相同來源。 從 developer.chrome.comwww.chrome.com 時,您無法進行轉換,因為這些轉換是跨來源且位於相同網站。


已選擇啟用跨文件檢視轉換功能

如要在兩份文件之間進行跨文件檢視轉換,雙方參與的網頁都必須選擇允許這項功能。方法是使用 CSS 中的 @view-transition at-rule 設定。

@view-transition at-rule 中,將 navigation 描述元設為 auto,即可啟用跨文件相同來源瀏覽的檢視轉換。

@view-transition {
  navigation: auto;
}

navigation 描述元設為 auto,即表示您選擇允許下列 NavigationType 執行檢視畫面轉換:

  • traverse
  • pushreplace (如果啟動作業不是由使用者透過瀏覽器 UI 機制啟動)。

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

如果瀏覽操作所需時間過長 (在 Chrome 中,這會超過 4 秒),系統會使用 TimeoutError DOMException 略過檢視畫面轉場。

跨文件檢視轉換示範

請觀看以下使用 View 轉換的示範,製作堆疊導覽工具示範。這裡沒有任何對 document.startViewTransition() 的呼叫,而檢視轉換是從一個頁面到另一個頁面時觸發。

堆疊導覽示範的記錄。需使用 Chrome 126 以上版本。

自訂跨文件檢視轉場效果

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

這些功能並非 View Transition API 規格本身的一部分,但應搭配運作方式使用。

pageswappagereveal 事件

瀏覽器支援

  • 124
  • 124
  • x
  • x

來源

為讓您自訂跨文件檢視畫面的轉場效果,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 則會解析。
  • pagereveal:現在的 updateCallBack 承諾已經解決。您可以使用 viewTransition.readyviewTransition.finished 承諾。

瀏覽器支援

  • 123
  • 123
  • x
  • x

來源

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

例如,在 MPA 堆疊導覽器中要使用的動畫類型取決於導覽路徑:

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

為此,您必須取得導覽相關資訊 (如果是 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);
    }
  }
});

等待內容透過轉譯封鎖功能載入

瀏覽器支援

  • 124
  • 124
  • x
  • x

來源

在某些情況下,您可能會想先暫停網頁首次轉譯,直到特定元素出現在新 DOM 中。這樣做可以避免閃爍,並且確保您動畫的狀態穩定。

<head> 中,使用以下中繼標記定義一或多個必須在網頁首次轉譯前顯示的元素 ID。

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

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

全面啟用轉譯封鎖功能前,請注意漸進式轉譯是網頁的基本要素,因此選擇封鎖顯示功能時請小心謹慎。需要評估封鎖算繪的影響。根據預設,除非您可以主動評估及評估網站體驗核心指標受到的影響,否則請避免使用 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;
  }
}

由於類型僅適用於 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 Working Group」提交問題,並附上建議和問題。請在 [css-view-transitions] 前方加上你的問題。 如果您遇到錯誤,請改為回報 Chromium 錯誤