如果兩個不同的文件之間發生檢視區塊轉場效果,則稱為跨文件檢視區塊轉場效果。多頁面應用程式 (MPA) 通常會發生這種情況。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 規則完成。
在 @view-transition at 規則中,將 navigation 描述元設為 auto,即可為跨文件、相同來源的導覽啟用檢視區塊轉場效果。
@view-transition {
navigation: auto;
}
將 navigation 描述元設為 auto,即表示您選擇允許下列 NavigationType 發生檢視畫面轉場效果:
traverse- 或
replace,如果啟用不是使用者透過瀏覽器 UI 機制啟動。push
從 auto 排除的導覽包括使用網址列導覽、點選書籤,以及任何形式的使用者或指令啟動重新載入。
如果導覽時間過長 (以 Chrome 來說,超過四秒),系統就會略過檢視區塊轉換,並顯示 TimeoutError DOMException。
跨文件檢視畫面轉場效果的示範
請參閱下列示範,瞭解如何使用檢視區塊轉場效果建立 Stack Navigator 示範。這裡沒有對 document.startViewTransition() 的呼叫,檢視區塊轉場效果是透過從一個頁面導覽至另一個頁面觸發。
自訂跨文件檢視畫面轉場效果
如要自訂跨文件檢視畫面轉場效果,可以使用一些網頁平台功能。
這些功能本身並非 View Transition API 規格的一部分,但設計上可與該規格搭配使用。
「pageswap」和「pagereveal」事件
為方便您自訂跨文件檢視畫面轉場效果,HTML 規格包含兩個可用的新事件:pageswap 和 pagereveal。
無論是否即將發生檢視區塊轉場效果,這兩個事件都會針對每個同源跨文件導覽觸發。如果兩個網頁之間即將發生檢視區塊轉換,您可以使用這些事件的 viewTransition 屬性存取 ViewTransition 物件。
pageswap事件會在網頁的最後一個影格算繪前觸發。您可以在擷取舊版快照前,使用這項功能對即將離開的網頁進行最後的變更。- 網頁初始化或重新啟動後,在第一次算繪機會前,會觸發
pagereveal事件。你可以先自訂新頁面,再拍攝新的快照。
舉例來說,您可以透過這些事件,從 sessionStorage 寫入及讀取資料,快速設定或變更部分 view-transition-name 值,或將資料從一個文件傳遞至另一個文件,在實際執行檢視區塊轉場效果前進行自訂。
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 物件是兩個不同的物件。此外,這兩種方法處理各種 Promise 的方式也不同:
pageswap:文件隱藏後,系統會略過舊的ViewTransition物件。這時,viewTransition.ready會拒絕,viewTransition.finished則會解決。pagereveal:此時updateCallBackpromise 已解決。您可以使用viewTransition.ready和viewTransition.finished承諾。
導航啟用資訊
在 pageswap 和 pagereveal 事件中,您也可以根據新舊網頁的網址採取行動。
舉例來說,在 MPA Stack Navigator 中,要使用的動畫類型取決於導覽路徑:
- 從總覽頁面導覽至詳細資料頁面時,新內容必須從右向左滑入。
- 從詳細資料頁面導覽至總覽頁面時,舊內容必須從左向右滑出。
如要執行這項操作,您需要導覽相關資訊。以 pageswap 來說,這類資訊與即將發生的導覽有關;以 pagereveal 來說,則與剛發生的導覽有關。
為此,瀏覽器現在可以公開 NavigationActivation 物件,其中包含同源導覽的相關資訊。這個物件會公開所用的導覽類型、目前和最終目的地記錄項目 (如 navigation.entries() 中的 Navigation API 所示)。
在已啟用的頁面上,您可以透過 navigation.activation 存取這個物件。在 pageswap 事件中,您可以透過 e.activation 存取這項資訊。
請參閱這個設定檔示範,瞭解如何使用 NavigationActivation 資訊,在 pageswap 和 pagereveal 事件中設定需要參與檢視區塊轉換的元素 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);
}
}
});
等待內容載入 (會造成轉譯遭到封鎖)
Browser Support
在某些情況下,您可能想延後網頁的首次算繪,直到新 DOM 中出現特定元素為止。這樣可避免閃爍,並確保動畫狀態穩定。
在 <head> 中,使用下列中繼標記定義一或多個元素 ID,這些 ID 必須存在,網頁才能進行首次算繪。
<link rel="expect" blocking="render" href="#section1">
這個中繼標記表示元素應存在於 DOM 中,而非內容應載入。舉例來說,如果是圖片,只要 DOM 樹狀結構中存在含有指定 id 的 <img> 標記,條件評估結果就會是 True。圖片本身可能仍在載入中。
請注意,在全力避免算繪遭到封鎖之前,請先瞭解遞增式算繪是網頁的基本要素,因此選擇封鎖算繪時請務必謹慎。封鎖算繪的影響應視個案情況評估。除非您能主動評估 blocking=render 對使用者的影響,並透過評估網站體驗核心指標來衡量影響程度,否則請避免使用這項功能。
在跨文件檢視畫面轉場效果中查看轉場類型
跨文件檢視區塊轉場效果也支援檢視區塊轉場效果類型,可自訂動畫和要擷取的元素。
舉例來說,在分頁中前往下一頁或上一頁時,您可能會想根據要前往序列中的較高或較低頁面,使用不同的動畫。
如要預先設定這些類型,請在 @view-transition at 規則中新增類型:
@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;
}
}
由於型別只適用於有效的檢視區塊轉換,因此檢視區塊轉換完成時,型別會自動清除。因此,型別可與 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 錯誤。