如果在兩個不同文件之間發生檢視畫面轉場,就稱為跨文件檢視畫面轉場。這通常是多頁應用程式 (MPA) 的情況。Chrome 自 Chrome 126 版開始支援跨文件檢視轉換功能。
瀏覽器支援
跨文件檢視轉換所需的建構模塊和原則與相同文件檢視轉換相同,這其實相當刻意:
- 瀏覽器會擷取舊版和新版中具有唯一
view-transition-name
的元素快照。 - 在渲染作業遭到抑制時,DOM 會更新。
- 最後,轉場效果是由 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 機制啟動,則為
push
或replace
。
auto
排除的導覽包括使用網址列或點選書籤進行導覽,以及任何形式的使用者或指令碼啟動重載。
如果導覽時間過長 (Chrome 為四秒以上),系統會使用 TimeoutError
DOMException
略過檢視轉場效果。
跨文件檢視畫面轉場示範
請查看以下使用檢視畫面轉場效果建立Stack Navigator 示範的示範影片。這裡沒有對 document.startViewTransition()
的呼叫,而是透過從一個頁面導覽至另一個頁面,觸發檢視畫面轉場效果。
自訂跨文件檢視畫面轉場效果
如要自訂跨文件檢視畫面的轉場效果,您可以使用一些網路平台功能。
這些功能並非 View Transition API 規範的一部分,但設計上可與該規範搭配使用。
pageswap
和 pagereveal
事件
為了讓您自訂跨文件檢視的轉場效果,HTML 規格包含兩個可以使用的新事件:pageswap
和 pagereveal
。
無論是否即將發生檢視轉場,每個同源跨文件導覽都會觸發這兩個事件。如果即將轉換的檢視畫面轉換會在這兩個頁面之間進行,您可以對這些事件使用 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();
}
}
}
pageswap
和 pagereveal
中的 ViewTransition
物件是兩個不同的物件。它們處理各種承諾的方式也不同:
pageswap
:隱藏文件後,系統會略過舊的ViewTransition
物件。發生這種情況時,viewTransition.ready
會拒絕,viewTransition.finished
會解析。pagereveal
:updateCallBack
已在這個時間點解決。您可以使用viewTransition.ready
和viewTransition.finished
承諾。
導航啟用資訊
在 pageswap
和 pagereveal
事件中,您也可以根據舊版和新版網頁的網址採取行動。
舉例來說,在 MPA Stack Navigator 中,所使用的動畫類型會視導覽路徑而定:
- 從總覽頁面前往詳細資料頁面時,新內容必須從右側滑入左側。
- 從詳細資料頁面前往總覽頁面時,舊內容必須從左至右滑出。
如要執行這項操作,您需要瞭解即將發生的導覽資訊 (pageswap
的情況),或是剛發生的導覽資訊 (pagereveal
的情況)。
為此,瀏覽器現在可公開 NavigationActivation
物件,該物件可保存同源導覽相關資訊。這個物件會公開已使用的導覽類型、目前和最終目的地記錄項目,如 navigation.entries()
中的 Navigation API 所示。
在已啟用的網頁上,您可以透過 navigation.activation
存取此物件。在 pageswap
事件中,您可以透過 e.activation
存取此值。
請參閱這個設定檔示範,瞭解如何在 pageswap
和 pagereveal
事件中使用 NavigationActivation
資訊,針對需要參與檢視畫面轉場的元素設定 view-transition-name
值。
這樣一來,您就不必事先為清單中的每個項目加上 view-transition-name
。相反地,這只用 JavaScript 及時發生,只對需要其用的元素。
程式碼如下:
// 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);
}
}
});
等待內容載入,並顯示轉譯阻斷畫面
瀏覽器支援
在某些情況下,您可能需要暫緩網頁的首次算繪,直到新 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;
}
如要即時設定類型,請使用 pageswap
和 pagereveal
事件來操作 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 等功能搭配使用。
示範
在以下分頁示範中,頁面內容會根據您前往的頁碼向前或向後滑動。
系統會透過網址內及網址在 pagereveal
和 pageswap
事件中,決定要使用的轉換類型。
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 錯誤。