使用檢視過渡 API
本文解釋了檢視過渡 API 的工作原理、如何建立檢視過渡和自定義過渡動畫,以及如何操作活動檢視過渡的理論。這涵蓋了單頁應用 (SPA) 中 DOM 狀態更新的檢視過渡,以及多頁應用 (MPA) 中文件之間的導航。
檢視過渡過程
讓我們逐步瞭解檢視過渡的工作過程
-
檢視過渡被觸發。其觸發方式取決於檢視過渡的型別
- 對於同文檔過渡 (SPA),透過將觸發檢視更改 DOM 更新的函式作為回撥傳遞給
document.startViewTransition()方法來觸發檢視過渡。 - 對於跨文件過渡 (MPA),透過啟動到新文件的導航來觸發檢視過渡。導航的當前文件和目標文件都需要位於相同的源,並透過在其 CSS 中包含一個
@view-transitionat-rule,其中navigation描述符設定為auto來選擇加入檢視過渡。注意:活動的檢視過渡具有關聯的
ViewTransition例項(例如,在同文檔 (SPA) 過渡的情況下由startViewTransition()返回)。ViewTransition物件包含多個 Promise,允許你在檢視過渡過程的不同部分達到時執行程式碼。有關更多資訊,請參閱使用 JavaScript 控制檢視過渡。
- 對於同文檔過渡 (SPA),透過將觸發檢視更改 DOM 更新的函式作為回撥傳遞給
-
在當前(舊頁面)檢視上,API 會捕獲已宣告
view-transition-name的元素的靜態影像快照。 -
檢視更改發生
-
對於同文檔過渡 (SPA),呼叫傳遞給
startViewTransition()的回撥,這會導致 DOM 更改。當回撥成功執行後,
ViewTransition.updateCallbackDonePromise 會實現,允許你響應 DOM 更新。 -
對於跨文件過渡 (MPA),導航發生在當前文件和目標文件之間。
-
-
API 從新檢視中捕獲“即時”快照(即互動式 DOM 區域)。
此時,檢視過渡即將執行,
ViewTransition.readyPromise 會實現,允許你透過執行自定義 JavaScript 動畫(而不是預設動畫)來響應,例如。 -
舊頁面快照“淡出”,新檢視快照“淡入”。預設情況下,舊檢視快照從
opacity1 動畫到 0,新檢視快照從opacity0 動畫到 1,從而建立交叉淡入淡出效果。 -
當過渡動畫達到其結束狀態時,
ViewTransition.finishedPromise 會實現,允許你響應。
注意:如果在 document.startViewTransition() 呼叫期間,文件的 頁面可見性狀態 為 hidden(例如,如果文件被視窗遮擋、瀏覽器最小化或另一個瀏覽器選項卡處於活動狀態),則檢視過渡將完全跳過。
關於快照的題外話
值得注意的是,在談論檢視過渡時,我們通常使用術語快照來指代頁面中聲明瞭 view-transition-name 的部分。這些部分將與頁面上設定了不同 view-transition-name 值的其他部分分開進行動畫處理。雖然透過檢視過渡對快照進行動畫處理實際上涉及兩個單獨的快照——一箇舊 UI 狀態的快照和一個新 UI 狀態的快照——但為簡單起見,我們使用快照來指代整個頁面區域。
舊 UI 狀態的快照是靜態影像,因此使用者無法在它“淡出”時與之互動。
新 UI 狀態的快照是互動式 DOM 區域,因此使用者可以在新內容“淡入”時開始與之互動。
檢視過渡偽元素樹
為了處理創建出站和入站過渡動畫,API 構建了一個具有以下結構的偽元素樹
::view-transition
└─ ::view-transition-group(root)
└─ ::view-transition-image-pair(root)
├─ ::view-transition-old(root)
└─ ::view-transition-new(root)
對於同文檔過渡 (SPA),偽元素樹在文件中可用。對於跨文件過渡 (MPA),偽元素樹僅在目標文件中可用。
樹結構中最有趣的部分如下
-
::view-transition是檢視過渡疊加層的根,它包含所有檢視過渡組並位於所有其他頁面內容之上。 -
::view-transition-group()作為每個檢視過渡快照的容器。root引數指定預設快照 — 檢視過渡動畫將應用於view-transition-name為root的快照。預設情況下,這是:root元素的快照,因為預設瀏覽器樣式定義了這一點css:root { view-transition-name: root; }但請注意,頁面作者可以透過取消上述設定並在不同元素上設定
view-transition-name: root來更改此設定。 -
::view-transition-old()目標是舊頁面元素的靜態快照,而::view-transition-new()目標是新頁面元素的即時快照。這兩者都作為替換內容呈現,方式與<img>或<video>相同,這意味著它們可以使用object-fit和object-position等屬性進行樣式設定。
注意:可以透過在不同的 DOM 元素上設定不同的 view-transition-name 來使用不同的自定義檢視過渡動畫來定位它們。在這種情況下,會為每個元素建立一個 ::view-transition-group()。有關示例,請參閱不同元素的動畫不同。
注意:正如你稍後將看到的,要自定義出站和入站動畫,你需要分別使用動畫來定位 ::view-transition-old() 和 ::view-transition-new() 偽元素。
建立基本的檢視過渡
本節演示瞭如何建立基本的檢視過渡,包括 SPA 和 MPA 的情況。
基本 SPA 檢視過渡
SPA 可能包括響應某種事件(例如導航連結被點選或伺服器推送更新)來獲取新內容並更新 DOM 的功能。
我們的 檢視過渡 SPA 演示 是一個基本的影像庫。我們有一系列包含縮圖 <img> 元素的 <a> 元素,使用 JavaScript 動態生成。我們還有一個包含 <figcaption> 和 <img> 的 <figure> 元素,用於顯示全尺寸相簿影像。
當點選縮圖時,displayNewImage() 函式透過 Document.startViewTransition() 執行,這會導致全尺寸影像及其關聯的標題顯示在 <figure> 中。我們已將其封裝在一個 updateView() 函式中,該函式僅在瀏覽器支援 View Transition API 時才呼叫它
function updateView(event) {
// Handle the difference in whether the event is fired on the <a> or the <img>
const targetIdentifier = event.target.firstChild || event.target;
const displayNewImage = () => {
const mainSrc = `${targetIdentifier.src.split("_th.jpg")[0]}.jpg`;
galleryImg.src = mainSrc;
galleryCaption.textContent = targetIdentifier.alt;
};
// Fallback for browsers that don't support View Transitions:
if (!document.startViewTransition) {
displayNewImage();
return;
}
// With View Transitions:
const transition = document.startViewTransition(() => displayNewImage());
}
此程式碼足以處理顯示影像之間的過渡。支援的瀏覽器會將舊影像和新影像以及標題的更改顯示為平滑的交叉淡入淡出(預設檢視過渡)。它仍然可以在不支援的瀏覽器中工作,但沒有漂亮的動畫。
基本 MPA 檢視過渡
在建立跨文件 (MPA) 檢視過渡時,過程甚至比 SPA 更簡單。不需要 JavaScript,因為檢視更新是由跨文件、同源導航觸發的,而不是由 JavaScript 啟動的 DOM 更改觸發的。要啟用基本的 MPA 檢視過渡,你需要在當前文件和目標文件的 CSS 中指定 @view-transition at-rule 以選擇加入,如下所示
@view-transition {
navigation: auto;
}
我們的 檢視過渡 MPA 演示 展示了此 at-rule 的實際應用,並額外演示瞭如何自定義檢視過渡的出站和入站動畫。
注意:目前 MPA 檢視過渡只能在同源文件之間建立,但此限制可能會在未來的實現中放寬。
自定義動畫
檢視過渡偽元素應用了預設的 CSS 動畫(在其參考頁面中有詳細說明)。
如上所述,大多數外觀過渡都具有預設的平滑交叉淡入淡出動畫。有一些例外
height和width過渡應用了平滑縮放動畫。position和transform過渡應用了平滑移動動畫。
你可以使用常規 CSS 以任何你想要的方式修改預設動畫 — 使用 ::view-transition-old() 針對“from”動畫,使用 ::view-transition-new() 針對“to”動畫。
例如,要更改兩者的速度
::view-transition-old(root),
::view-transition-new(root) {
animation-duration: 0.5s;
}
建議你在想要將樣式應用於 ::view-transition-old() 和 ::view-transition-new() 的情況下,使用 ::view-transition-group() 來定位這些樣式。由於偽元素層次結構和預設的使用者代理樣式,這些樣式將被兩者繼承。例如
::view-transition-group(root) {
animation-duration: 0.5s;
}
注意:這也是一種保護程式碼的好選擇——::view-transition-group() 也會動畫,你可能會得到 group/image-pair 偽元素與 old 和 new 偽元素的不同持續時間。
對於跨文件 (MPA) 過渡,偽元素需要僅包含在目標文件中才能使檢視過渡工作。如果你想在兩個方向上都使用檢視過渡,則需要將它包含在兩者中。
我們的 檢視過渡 MPA 演示 包含了上述 CSS,但將自定義更進一步,定義了自定義動畫並將其應用於 ::view-transition-old(root) 和 ::view-transition-new(root) 偽元素。結果是,當導航發生時,預設的交叉淡入淡出過渡被替換為“向上滑動”過渡
/* Create a custom animation */
@keyframes move-out {
from {
transform: translateY(0%);
}
to {
transform: translateY(-100%);
}
}
@keyframes move-in {
from {
transform: translateY(100%);
}
to {
transform: translateY(0%);
}
}
/* Apply the custom animation to the old and new page states */
::view-transition-old(root) {
animation: 0.4s ease-in both move-out;
}
::view-transition-new(root) {
animation: 0.4s ease-in both move-in;
}
不同元素的動畫不同
預設情況下,檢視更新期間更改的所有不同元素都使用相同的動畫進行過渡。如果你希望某些元素與預設的 root 動畫不同地進行動畫處理,你可以使用 view-transition-name 屬性將它們分開。例如,在我們的 檢視過渡 SPA 演示 中,<figcaption> 元素被賦予 figure-caption 的 view-transition-name,以便在檢視過渡方面將它們與頁面的其餘部分分開
figcaption {
view-transition-name: figure-caption;
}
應用此 CSS 後,生成的偽元素樹現在將如下所示
::view-transition
├─ ::view-transition-group(root)
│ └─ ::view-transition-image-pair(root)
│ ├─ ::view-transition-old(root)
│ └─ ::view-transition-new(root)
└─ ::view-transition-group(figure-caption)
└─ ::view-transition-image-pair(figure-caption)
├─ ::view-transition-old(figure-caption)
└─ ::view-transition-new(figure-caption)
第二組偽元素的存在允許僅對 <figcaption> 應用單獨的檢視過渡樣式。不同的舊檢視和新檢視捕獲彼此獨立處理。
以下程式碼僅對 <figcaption> 應用自定義動畫
@keyframes grow-x {
from {
transform: scaleX(0);
}
to {
transform: scaleX(1);
}
}
@keyframes shrink-x {
from {
transform: scaleX(1);
}
to {
transform: scaleX(0);
}
}
::view-transition-group(figure-caption) {
height: auto;
right: 0;
left: auto;
transform-origin: right center;
}
::view-transition-old(figure-caption) {
animation: 0.25s linear both shrink-x;
}
::view-transition-new(figure-caption) {
animation: 0.25s 0.25s linear both grow-x;
}
在這裡,我們建立了一個自定義 CSS 動畫,並將其應用於 ::view-transition-old(figure-caption) 和 ::view-transition-new(figure-caption) 偽元素。我們還為兩者添加了許多其他樣式,以使它們保持在相同的位置,並阻止預設樣式干擾我們的自定義動畫。
注意:你可以使用 * 作為偽元素中的識別符號來定位所有快照偽元素,無論它們具有什麼名稱。例如
::view-transition-group(*) {
animation-duration: 2s;
}
有效的 view-transition-name 值
view-transition-name 屬性可以採用唯一的 <custom-ident> 值,它可以是任何不會被誤解為關鍵字的識別符號。每個渲染元素的 view-transition-name 值必須是唯一的。如果兩個渲染元素同時具有相同的 view-transition-name,ViewTransition.ready 將被拒絕,並且過渡將被跳過。
它還可以採用關鍵字值
none:使元素不參與單獨的快照,除非它有一個設定了view-transition-name的父元素,在這種情況下,它將作為該元素的一部分進行快照。match-element:自動為所有選定元素設定唯一的view-transition-name值。
利用預設動畫樣式
請注意,我們還發現了另一種更簡單且比上述方法產生更好結果的過渡選項。我們最終的 <figcaption> 檢視過渡最終看起來像這樣
figcaption {
view-transition-name: figure-caption;
}
::view-transition-group(figure-caption) {
height: 100%;
}
這之所以有效,是因為預設情況下,::view-transition-group() 會以平滑縮放方式在舊檢視和新檢視之間過渡 width 和 height。我們只需要為兩種狀態設定固定的 height 即可使其工作。
注意:使用檢視過渡 API 實現平滑過渡 包含其他幾個自定義示例。
使用 JavaScript 控制檢視過渡
檢視過渡具有關聯的 ViewTransition 物件例項,其中包含多個 Promise 成員,允許你響應過渡的不同狀態到達時執行 JavaScript。例如,一旦建立了偽元素樹並且動畫即將開始,ViewTransition.ready 就會實現,而一旦動畫完成並且新的頁面檢視對使用者可見和可互動,ViewTransition.finished 就會實現。
可以這樣訪問 ViewTransition
- 對於同文檔 (SPA) 過渡,
document.startViewTransition()方法返回與過渡關聯的ViewTransition。 - 對於跨文件 (MPA) 過渡
- 當文件由於導航而即將解除安裝時,會觸發
pageswap事件。其事件物件 (PageSwapEvent) 透過PageSwapEvent.viewTransition屬性提供對ViewTransition的訪問,以及透過PageSwapEvent.activation提供包含導航型別以及當前和目標文件歷史條目的NavigationActivation。注意:如果導航在重定向鏈中的任何位置都有跨域 URL,則
activation屬性返回null。 - 當文件首次渲染時(無論是從網路載入新文件還是啟用文件(從 回退/前進快取 (bfcache) 或 預渲染)),都會觸發
pagereveal事件。其事件物件 (PageRevealEvent) 透過PageRevealEvent.viewTransition屬性提供對ViewTransition的訪問。
- 當文件由於導航而即將解除安裝時,會觸發
讓我們看一些示例程式碼,以展示如何使用這些功能。
JavaScript 驅動的自定義同文檔 (SPA) 過渡
以下 JavaScript 可用於建立從使用者點選時滑鼠游標位置發出的圓形顯示檢視過渡,動畫由 Web Animations API 提供。
// Store the last click event
let lastClick;
addEventListener("click", (event) => (lastClick = event));
function spaNavigate(data) {
// Fallback for browsers that don't support this API:
if (!document.startViewTransition) {
updateTheDOMSomehow(data);
return;
}
// Get the click position, or fallback to the middle of the screen
const x = lastClick?.clientX ?? innerWidth / 2;
const y = lastClick?.clientY ?? innerHeight / 2;
// Get the distance to the furthest corner
const endRadius = Math.hypot(
Math.max(x, innerWidth - x),
Math.max(y, innerHeight - y),
);
// Create a transition:
const transition = document.startViewTransition(() => {
updateTheDOMSomehow(data);
});
// Wait for the pseudo-elements to be created:
transition.ready.then(() => {
// Animate the root's new view
document.documentElement.animate(
{
clipPath: [
`circle(0 at ${x}px ${y}px)`,
`circle(${endRadius}px at ${x}px ${y}px)`,
],
},
{
duration: 500,
easing: "ease-in",
// Specify which pseudo-element to animate
pseudoElement: "::view-transition-new(root)",
},
);
});
}
此動畫還需要以下 CSS,以關閉預設 CSS 動畫並阻止舊檢視和新檢視狀態以任何方式混合(新狀態“擦拭”舊狀態,而不是過渡進入)
::view-transition-image-pair(root) {
isolation: auto;
}
::view-transition-old(root),
::view-transition-new(root) {
animation: none;
mix-blend-mode: normal;
display: block;
}
JavaScript 驅動的自定義跨文件 (MPA) 過渡
Chrome DevRel 團隊成員列表 演示提供了一組基本的團隊資料頁面,並演示瞭如何使用 pageswap 和 pagereveal 事件根據“from”和“to”URL 自定義跨文件檢視過渡的出站和入站動畫。
pageswap 事件監聽器如下所示。這會在連結到資料頁面的出站頁面上的元素上設定檢視過渡名稱。當從主頁導航到資料頁面時,自定義動畫僅針對每次點選的連結元素提供。
window.addEventListener("pageswap", async (e) => {
// Only run this if an active view transition exists
if (e.viewTransition) {
const currentUrl = e.activation.from?.url
? new URL(e.activation.from.url)
: null;
const targetUrl = new URL(e.activation.entry.url);
// Going from profile page to homepage
// ~> The big img and title are the ones!
if (isProfilePage(currentUrl) && isHomePage(targetUrl)) {
// Set view-transition-name values on the elements to animate
document.querySelector(`#detail main h1`).style.viewTransitionName =
"name";
document.querySelector(`#detail main img`).style.viewTransitionName =
"avatar";
// Remove view-transition-names after snapshots have been taken
// Stops naming conflicts resulting from the page state persisting in BFCache
await e.viewTransition.finished;
document.querySelector(`#detail main h1`).style.viewTransitionName =
"none";
document.querySelector(`#detail main img`).style.viewTransitionName =
"none";
}
// Going to profile page
// ~> The clicked items are the ones!
if (isProfilePage(targetUrl)) {
const profile = extractProfileNameFromUrl(targetUrl);
// Set view-transition-name values on the elements to animate
document.querySelector(`#${profile} span`).style.viewTransitionName =
"name";
document.querySelector(`#${profile} img`).style.viewTransitionName =
"avatar";
// Remove view-transition-names after snapshots have been taken
// Stops naming conflicts resulting from the page state persisting in BFCache
await e.viewTransition.finished;
document.querySelector(`#${profile} span`).style.viewTransitionName =
"none";
document.querySelector(`#${profile} img`).style.viewTransitionName =
"none";
}
}
});
注意:我們在每次拍攝快照後刪除 view-transition-name 值。如果我們保留它們設定,它們將保留在導航時儲存到 bfcache 的頁面狀態中。如果然後按下返回按鈕,則導航回的頁面的 pagereveal 事件處理程式將嘗試在不同的元素上設定相同的 view-transition-name 值。如果多個元素設定了相同的 view-transition-name,則檢視過渡將被跳過。
pagereveal 事件監聽器如下所示。這與 pageswap 事件監聽器類似,但請記住,這裡我們正在為新頁面上的頁面元素自定義“to”動畫。
window.addEventListener("pagereveal", async (e) => {
// If the "from" history entry does not exist, return
if (!navigation.activation.from) return;
// Only run this if an active view transition exists
if (e.viewTransition) {
const fromUrl = new URL(navigation.activation.from.url);
const currentUrl = new URL(navigation.activation.entry.url);
// Went from profile page to homepage
// ~> Set VT names on the relevant list item
if (isProfilePage(fromUrl) && isHomePage(currentUrl)) {
const profile = extractProfileNameFromUrl(fromUrl);
// Set view-transition-name values on the elements to animate
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";
}
// Went to profile page
// ~> Set VT names on the main title and image
if (isProfilePage(currentUrl)) {
// Set view-transition-name values on the elements to animate
document.querySelector(`#detail main h1`).style.viewTransitionName =
"name";
document.querySelector(`#detail main 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(`#detail main h1`).style.viewTransitionName =
"none";
document.querySelector(`#detail main img`).style.viewTransitionName =
"none";
}
}
});
穩定頁面狀態以使跨文件過渡保持一致
在執行跨文件過渡之前,你理想地希望等待頁面狀態穩定下來,依靠 渲染阻塞 來確保
- 關鍵樣式已載入並應用。
- 關鍵指令碼已載入並執行。
- 使用者初始檢視可見的 HTML 已解析,因此它會一致地渲染。
預設情況下,樣式是渲染阻塞的,除非它們透過指令碼動態新增到文件中。指令碼和動態新增的樣式都可以使用 blocking="render" 屬性進行渲染阻塞。
為了確保你的初始 HTML 已解析並在過渡動畫執行之前始終一致地渲染,你可以使用 <link rel="expect">。在此元素中,你包含以下屬性
rel="expect"表示你想要使用此<link>元素來渲染阻塞頁面上的某些 HTML。href="#element-id"表示你想要渲染阻塞的元素的 ID。blocking="render"用於渲染阻塞指定的 HTML。
注意:為了阻塞渲染,具有 blocking="render" 的 script、link 和 style 元素必須位於文件的 head 中。
讓我們透過一個 HTML 文件示例來探討這是什麼樣子
<!doctype html>
<html lang="en">
<head>
<!-- This will be render-blocking by default -->
<link rel="stylesheet" href="style.css" />
<!-- Marking critical scripts as render blocking will
ensure they're run before the view transition is activated -->
<script async src="layout.js" blocking="render"></script>
<!-- Use rel="expect" and blocking="render" to ensure the
#lead-content element is visible and fully parsed before
activating the transition -->
<link rel="expect" href="#lead-content" blocking="render" />
</head>
<body>
<h1>Page title</h1>
<nav>...</nav>
<div id="lead-content">
<section id="first-section">The first section</section>
<section>The second section</section>
</div>
</body>
</html>
結果是文件渲染被阻塞,直到解析了主要內容 <div>,確保了一致的檢視過渡。
你還可以在 <link rel="expect"> 元素上指定 media 屬性。例如,你可能希望在窄屏裝置上載入頁面時阻塞較少內容的渲染,而不是在寬屏裝置上。這是有道理的——在移動裝置上,頁面首次載入時可見的內容比在桌面裝置上少。
這可以透過以下 HTML 實現
<link
rel="expect"
href="#lead-content"
blocking="render"
media="screen and (width > 640px)" />
<link
rel="expect"
href="#first-section"
blocking="render"
media="screen and (width <= 640px)" />