เมื่อเกิดการเปลี่ยนมุมมองระหว่างเอกสาร 2 ฉบับที่แตกต่างกัน เราจะเรียกว่าการเปลี่ยนมุมมองข้ามเอกสาร ซึ่งโดยทั่วไปจะเป็นกรณีในแอปพลิเคชันแบบหลายหน้า (MPA) Chrome รองรับการเปลี่ยนฉากมุมมองข้ามเอกสารตั้งแต่ Chrome 126 เป็นต้นไป
การเปลี่ยนมุมมองข้ามเอกสารใช้บล็อกและหลักการเดียวกันกับการเปลี่ยนมุมมองในเอกสารเดียวกัน ซึ่งเป็นสิ่งที่ตั้งใจไว้
- เบราว์เซอร์จะถ่ายภาพรวมขององค์ประกอบที่มี
view-transition-nameที่ไม่ซ้ำกันทั้งในหน้าเว็บเก่าและใหม่ - DOM จะได้รับการอัปเดตขณะที่การแสดงผลถูกระงับ
- และสุดท้าย การเปลี่ยนจะขับเคลื่อนด้วยภาพเคลื่อนไหว CSS
สิ่งที่แตกต่างเมื่อเทียบกับการเปลี่ยนมุมมองในเอกสารเดียวกันคือการเปลี่ยนมุมมองข้ามเอกสารนั้นไม่จำเป็นต้องเรียกใช้ document.startViewTransition เพื่อเริ่มการเปลี่ยนมุมมอง แต่ทริกเกอร์สำหรับการเปลี่ยนฉากมุมมองแบบข้ามเอกสารคือการไปยังส่วนต่างๆ ที่มีต้นทางเดียวกันจากหน้าหนึ่งไปยังอีกหน้าหนึ่ง ซึ่งเป็นการดำเนินการที่โดยปกติแล้วผู้ใช้เว็บไซต์ของคุณจะทำโดยการคลิกลิงก์
กล่าวคือ ไม่มี API ที่จะเรียกใช้เพื่อเริ่มการเปลี่ยนมุมมองระหว่างเอกสาร 2 ฉบับ อย่างไรก็ตาม มีเงื่อนไข 2 ข้อที่ต้องปฏิบัติตาม
- เอกสารทั้ง 2 ฉบับต้องอยู่ในต้นทางเดียวกัน
- ทั้ง 2 หน้าต้องเลือกใช้เพื่ออนุญาตการเปลี่ยนฉาก
เราจะอธิบายเงื่อนไขทั้ง 2 ข้อนี้ในภายหลังของเอกสารนี้
การเปลี่ยนฉากมุมมองข้ามเอกสารจะจำกัดอยู่เฉพาะการนำทางแบบต้นทางเดียวกัน
การเปลี่ยนฉากระหว่างเอกสารจะจำกัดไว้เฉพาะการนำทางแบบต้นทางเดียวกันเท่านั้น ระบบจะถือว่าการนำทางเป็นแบบต้นทางเดียวกันหากต้นทางของทั้ง 2 หน้าที่เข้าร่วมเหมือนกัน
ต้นทางของหน้าเว็บคือชุดค่าผสมของรูปแบบ ชื่อโฮสต์ และพอร์ตที่ใช้ ดังรายละเอียดใน web.dev
เช่น คุณสามารถมีการเปลี่ยนฉากมุมมองข้ามเอกสารเมื่อไปยังส่วนต่างๆ จาก developer.chrome.com ไปยัง developer.chrome.com/blog เนื่องจากทั้ง 2 ส่วนมาจากต้นทางเดียวกัน
คุณจะใช้การเปลี่ยนฉากดังกล่าวไม่ได้เมื่อไปยังส่วนต่างๆ จาก developer.chrome.com ไปยัง www.chrome.com เนื่องจากเป็นแบบข้ามต้นทางและแบบเว็บไซต์เดียวกัน
การเปลี่ยนมุมมองระหว่างเอกสารเป็นแบบเลือกใช้
หากต้องการให้การเปลี่ยนมุมมองข้ามเอกสารระหว่างเอกสาร 2 ฉบับเกิดขึ้นได้ ทั้ง 2 หน้าที่เกี่ยวข้องจะต้องเลือกใช้การตั้งค่านี้ ซึ่งทำได้ด้วยกฎ @ ใน CSS@view-transition
ใน@view-transition at-rule ให้ตั้งค่าตัวอธิบาย navigation เป็น auto เพื่อเปิดใช้การเปลี่ยนฉากมุมมองสำหรับการไปยังส่วนต่างๆ ในเอกสารที่ต่างกันแต่มีต้นทางเดียวกัน
@view-transition {
navigation: auto;
}
การตั้งค่าตัวอธิบาย navigation เป็น auto หมายความว่าคุณเลือกที่จะอนุญาตให้เกิดการเปลี่ยนมุมมองสำหรับ NavigationType ต่อไปนี้
traversepushหรือreplaceหากผู้ใช้ไม่ได้เริ่มการเปิดใช้งานผ่านกลไก UI ของเบราว์เซอร์
ตัวอย่างการนำทางที่ยกเว้นจาก auto คือการนำทางโดยใช้แถบที่อยู่ URL หรือการคลิกบุ๊กมาร์ก รวมถึงการโหลดซ้ำที่ผู้ใช้หรือสคริปต์เริ่มต้นในรูปแบบใดก็ตาม
หากการนำทางใช้เวลานานเกินไป (มากกว่า 4 วินาทีในกรณีของ Chrome) ระบบจะข้ามการเปลี่ยนฉากด้วย TimeoutError DOMException
การเปลี่ยนมุมมองระหว่างเอกสาร
ดูเดโมต่อไปนี้ที่ใช้การเปลี่ยนมุมมองเพื่อสร้างเดโม Stack Navigator ไม่มีการเรียกใช้ document.startViewTransition() ที่นี่ การเปลี่ยนมุมมองจะทริกเกอร์โดยการไปยังหน้าหนึ่งไปยังอีกหน้าหนึ่ง
ปรับแต่งการเปลี่ยนมุมมองระหว่างเอกสาร
หากต้องการปรับแต่งการเปลี่ยนมุมมองข้ามเอกสาร คุณสามารถใช้ฟีเจอร์บางอย่างของแพลตฟอร์มเว็บได้
ฟีเจอร์เหล่านี้ไม่ได้เป็นส่วนหนึ่งของข้อกำหนด View Transition API แต่ได้รับการออกแบบมาเพื่อใช้ร่วมกับ API ดังกล่าว
กิจกรรม pageswap และ pagereveal
ข้อกำหนด HTML มีเหตุการณ์ใหม่ 2 รายการที่คุณใช้ได้ ได้แก่ pageswap และ pagereveal เพื่อให้คุณปรับแต่งการเปลี่ยนมุมมองเอกสารได้
ระบบจะทริกเกอร์เหตุการณ์ทั้ง 2 นี้สำหรับการนำทางข้ามเอกสารที่มีแหล่งที่มาเดียวกันทุกครั้ง ไม่ว่าการเปลี่ยนฉากจะเกิดขึ้นหรือไม่ก็ตาม หากกำลังจะมีการเปลี่ยนมุมมองระหว่าง 2 หน้า คุณจะเข้าถึงออบเจ็กต์ 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');
}
});
หากต้องการ คุณสามารถเลือกข้ามการเปลี่ยนฉากในทั้ง 2 เหตุการณ์ได้
window.addEventListener("pagereveal", async (e) => {
if (e.viewTransition) {
if (goodReasonToSkipTheViewTransition()) {
e.viewTransition.skipTransition();
}
}
}
ออบเจ็กต์ ViewTransition ใน pageswap และ pagereveal เป็นออบเจ็กต์ 2 รายการที่แตกต่างกัน นอกจากนี้ ยังจัดการสัญญาต่างๆ แตกต่างกันด้วย ดังนี้
pageswap: เมื่อซ่อนเอกสารแล้ว ระบบจะข้ามออบเจ็กต์ViewTransitionเก่า เมื่อเกิดเหตุการณ์ดังกล่าวviewTransition.readyจะปฏิเสธและviewTransition.finishedจะแก้ไขpagereveal: ตอนนี้สัญญาupdateCallBackได้รับการแก้ไขแล้ว คุณใช้สัญญาviewTransition.readyและviewTransition.finishedได้
ข้อมูลการเปิดใช้งานการนำทาง
ในเหตุการณ์ pageswap และ pagereveal คุณยังดำเนินการตาม URL ของหน้าเว็บเก่าและหน้าเว็บใหม่ได้ด้วย
ตัวอย่างเช่น ใน 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> ให้กำหนดรหัสองค์ประกอบอย่างน้อย 1 รายการที่ต้องมีก่อนที่หน้าจะแสดงผลครั้งแรกโดยใช้เมตาแท็กต่อไปนี้
<link rel="expect" blocking="render" href="#section1">
แท็ก Meta นี้หมายความว่าองค์ประกอบควรอยู่ใน DOM ไม่ใช่ว่าควรโหลดเนื้อหา ตัวอย่างเช่น ในกรณีของรูปภาพ เพียงแค่มีแท็ก <img> ที่มี id ที่ระบุในแผนผัง DOM ก็เพียงพอแล้วที่เงื่อนไขจะประเมินค่าเป็นจริง รูปภาพอาจยังโหลดไม่เสร็จ
ก่อนที่จะมุ่งเน้นการบล็อกการแสดงผล โปรดทราบว่าการแสดงผลแบบเพิ่มทีละส่วนเป็นแง่มุมพื้นฐานของเว็บ ดังนั้นโปรดระมัดระวังเมื่อเลือกที่จะบล็อกการแสดงผล ผลกระทบของการบล็อกการแสดงผลต้องได้รับการประเมินเป็นรายกรณี โดยค่าเริ่มต้น ให้หลีกเลี่ยงการใช้ blocking=render เว้นแต่คุณจะวัดและประเมินผลกระทบที่มีต่อผู้ใช้ได้อย่างชัดเจนด้วยการวัดผลกระทบต่อ Core Web Vitals
ดูประเภทการเปลี่ยนในภาพเคลื่อนไหวของการเปลี่ยนมุมมองข้ามเอกสาร
การเปลี่ยนฉากมุมมองข้ามเอกสารยังรองรับประเภทการเปลี่ยนฉากมุมมองเพื่อปรับแต่งภาพเคลื่อนไหวและองค์ประกอบที่จะบันทึกด้วย
ตัวอย่างเช่น เมื่อไปที่หน้าถัดไปหรือหน้าก่อนหน้าในการแบ่งหน้า คุณอาจต้องการใช้ภาพเคลื่อนไหวที่แตกต่างกัน ทั้งนี้ขึ้นอยู่กับว่าคุณกำลังไปที่หน้าที่มีหมายเลขสูงขึ้นหรือต่ำลงจากลำดับ
หากต้องการตั้งค่าประเภทเหล่านี้ล่วงหน้า ให้เพิ่มประเภทในกฎ @view-transition @ ดังนี้
@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 ของหน้าเว็บใหม่โดยอัตโนมัติ คุณต้องกำหนดประเภทที่จะใช้ในหน้าใหม่อย่างน้อย 1 หน้าเพื่อให้ภาพเคลื่อนไหวทํางานได้ตามที่คาดไว้
หากต้องการตอบสนองต่อประเภทเหล่านี้ ให้ใช้ตัวเลือก: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 โดยดูที่ URL ต้นทางและปลายทาง
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';
}
};
ความคิดเห็น
เรายินดีรับฟังความคิดเห็นจากนักพัฒนาแอปเสมอ หากต้องการแชร์ โปรดแจ้งปัญหาต่อกลุ่มงาน CSS ใน GitHub พร้อมคำแนะนำและคำถาม นำหน้าปัญหาด้วย [css-view-transitions]
หากพบข้อบกพร่อง โปรดรายงานข้อบกพร่องของ Chromium แทน