Membangun transisi halaman asinkron dalam vanilla JavaScript – Beragampengetahuan
Hai! Saya Valentin Mor, pengembang kreatif front-end yang tinggal di Paris. Dalam tutorial ini kita akan membangun sistem konversi halaman asinkron ringan dari awal menggunakan vanilla JavaScript, GSAPDan Lebih jauh lagi.
Pada akhirnya, Anda akan memiliki router SPA yang berfungsi penuh yang dapat memudar antar halaman—tidak perlu bingkai.
memperkenalkan
Ini mungkin terdengar mengejutkan, tetapi ketika saya memikirkan logo situs web kreatif sebagai pengembang, hal pertama yang terlintas dalam pikiran saya adalah bagaimana menangani perutean dan transisi halaman. Terkadang yang Anda butuhkan hanyalah efek fade out/fade in yang sederhana, namun menambahkan sedikit kedalaman dan gerakan dapat meningkatkan pengalaman pengguna secara signifikan.
Saya menghabiskan banyak waktu menjelajahi topik ini menggunakan perpustakaan berikut Barba.js Pahami apa yang terjadi di balik layar – terutama ketika halaman saat ini dan halaman berikutnya hidup berdampingan sebentar di DOM.
Saya tidak bisa mengabaikan Aristide Benoist – dia adalah referensi nyata dalam hal transisi halaman sinematik yang mulus. Transisi yang kami bangun di sini terinspirasi oleh karyanya di situs Watson. Jika Anda belum mengetahuinya, saya sangat menyarankan untuk memeriksanya.
apa yang sedang kita bangun
Aplikasi satu halaman minimal dengan:
- Klien khusus router Cegat klik tautan dan kelola navigasi menggunakan History API.
- satu Mesin transformasi asinkron Tambahkan efek animasi ke halaman saat ini dan halaman berikutnya secara bersamaan.
Inilah ide kuncinya: Daripada langsung bertukar konten halaman, kami Wadah halaman klonmenyuntikkan konten baru ke dalam klon, menganimasikan kedua kontainer (yang lama keluar, yang baru masuk), lalu menghapus yang lama. Ini menciptakan transisi crossfade yang sebenarnya, di mana dua halaman hidup berdampingan di DOM selama animasi.
Pengaturan proyek
Buka IDE favorit Anda dan jalankan perintah berikut: npm create vite@latest
Saat diminta, pilih Vanilla sebagai kerangka dan JavaScript sebagai varian.
Bersihkan file awal dengan menghapus counter.js Berkas dari src folder dan simpan hanya impor gaya di folder Anda main.js.
yourproject/
├── node_modules/
├── public/
├── src/
│ ├── main.js
│ └── style.css
├── gitignore
├── index.html
├── package-lock.json
└── package.json
Langkah 1: Cangkang HTML
kita index.html File yang digunakan sebagai tata letak root – shell permanen yang tetap ada di seluruh navigasi. hanya isi di dalamnya data-transition="container" mengubah.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Building Async Page Transitions in Vanilla JavaScript</title>
</head>
<body>
<div data-transition="wrapper">
<div data-transition="container" data-namespace="home">
<main id="page_content" class="page_content"></main>
</div>
</div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
Tiga atribut data melakukan pekerjaan berat:
data-transition="wrapper"— Menyimpan kontainer saat ini dan elemen induk dari kontainer masuk selama transformasi. Kedua halaman ada di sini pada waktu yang sama.data-transition="container"— Buat tiruan untuk setiap halaman baru. Selama transformasi, pembungkus untuk sementara berisi dua elemen.data-namespace— Mengidentifikasi halaman yang sedang ditampilkan. Dalam tutorial ini hal ini terutama digunakan untuk debugging, namun dalam proyek yang lebih maju, menjadi penting untuk memetakan animasi transisi yang berbeda ke rute halaman-ke-halaman tertentu.
Langkah 2: Modul Halaman
Pertama buat a /pages folder di dalam srclalu tambahkan dua halaman pertama folder: /home Dan /alternative-page.
Setiap folder akan berisi file HTML dan file JavaScript.
├── pages/
│ ├── home/
│ │ ├── home.html
│ │ └── home.js
│ └── alternative-page/
│ ├── alternative-page.html
│ └── alternative-page.js
Tulis beberapa HTML minimal <section> kertas kado, satu <h1>dan sebuah <nav> Berisi link ke dua halaman website kami: “/” Dan”/alternative-page“.
<section class="hero">
<nav>
<a href="/alternative-page">Alternative page</a>
</nav>
<div class="hero_content">
<h1>AH.736</h1>
</div>
</section>
Setiap halaman adalah modul independen dan mengekspor fungsi default yang mengembalikan HTML.
Untuk kelengkapan, tambahkan a init() Fungsi untuk pengaturan JavaScript Anda (seperti pendengar acara), dan opsional cleanup() Fungsi pembongkaran.
Kami hanya akan menggunakan fungsi default untuk bagian inti tutorial ini.
ini ?raw suffix adalah fitur Vite yang dapat mengimpor file HTML sebagai string mentah. Templat yang sesuai adalah HTML murni:
import template from "./home.html?raw";
export default function HomePage()
return template;
export function init()
export function cleanup()
Sekarang, tambahkan beberapa gaya minimal untuk menjaga konten halaman pada ketinggian layar penuh, impor font favorit Anda, dan mulai tambahkan beberapa gaya dasar pilihan Anda.
@font-face
font-family: "Neue";
src: url("/NeueMontreal-Medium.ttf");
font-weight: 600;
font-style: normal;
font-display: swap;
:root
font-family: Neue;
line-height: 1;
color: rgb(0, 0, 0);
background-color: #000000;
*,
*::before,
*::after
box-sizing: border-box;
body
font-family: Neue;
margin: 0;
display: flex;
overflow-x: hidden;
min-height: 100vh;
main
width: 100vw;
h1
font-size: 28.2vw;
margin: 0;
line-height: 80%;
a
color: black;
text-decoration: none;
text-transform: uppercase;
.hero
background-color: white;
width: 100%;
height: 100vh;
overflow: hidden;
display: flex;
justify-content: space-between;
flex-direction: column;
padding: 20px;
align-items: center;
.hero_content
width: 100%;
padding-top: 8vh;
text-align: center;
[data-transition="container"]
transform: translateZ(0);
backface-visibility: hidden;
Catatan: Kami tidak akan membahas gaya atau animasi input yang rumit, karena topik ini sudah cukup padat. Saya sarankan untuk tetap berpegang pada batas minimum untuk saat ini dan kemudian meningkatkan proyek Anda dengan gaya dan animasi yang mewah.
Langkah 3: Perute
Router adalah otak dari SPA kami. Ini memotong klik tautan, mengelola riwayat browser, memuat modul halaman secara dinamis, dan mengoordinasikan transisi antar halaman.
Mari kita membangunnya sepotong demi sepotong.
di dalam src folder, buat a router.js dokumen.
definisi rute
Setiap rute memetakan jalur URL ke dua hal: namespace — Pengidentifikasi string yang menandai setiap halaman.
Mesin konversi akan menggunakan ini untuk menentukan halaman mana yang masuk dan halaman mana yang keluar.
loader adalah fungsi panah yang membungkus dinamika import().
Modul ini hanya didapat dari jaringan saat kita benar-benar menelepon loader().
Pada panggilan pertama, browser mengunduh dan mem-parsing modul; pada panggilan berikutnya, ia segera mengembalikan versi cache.
const routes =
"/":
namespace: "home",
loader: () => import("./pages/home/home.js"),
,
"/alternative-page":
namespace: "alternative-page",
loader: () => import("./pages/alternative-page/alternative-page.js"),
,
;
Struktur kelas
Setelah menentukan rute, mari buat a Router Kelas untuk mengatur status navigasi kita. Ia memiliki dua properti:
currentNamespaceMelacak halaman yang sedang ditampilkan, memungkinkan kita melewati navigasi ke halaman yang sama.isTransitioningMerupakan kunci yang mencegah navigasi ganda ketika pengguna mengklik dengan cepat.
Di bagian bawah file, kami mengekspor instance kelas ini.
class Router
constructor()
this.currentNamespace = null;
this.isTransitioning = false;
// Write your functions here
export const router = new Router();
Mari tambahkan fitur router – masing-masing dengan tujuan tertentu.
memuat halaman awal()
Metode ini dijalankan satu kali saat startup.
- Cocokkan URL saat ini dengan rutenya.
- Dapatkan jalur saat ini.
- Bandingkan dengan jalur kita
routesArray untuk memperlambat memuat modul yang benar. - Memuat modul.
- Suntikkan HTML-nya ke dalam
#page_contentElemen tersebut sudah ada di shell HTML kita. - mendirikan
namespaceLacak halaman yang sedang ditampilkan di penampung.
Tidak ada animasi transisi di sini – halaman muncul begitu saja.
async loadInitialPage()
// Match the current URL to a route
const path = window.location.pathname;
const route = routes[path]
// Dynamically import the page module
const pageModule = await route.loader();
// Inject the page's HTML template into the existing DOM shell
const content = document.getElementById("page_content");
content.innerHTML = pageModule.default();
// Tag the container with the current namespace
const container = document.querySelector('[data-transition="container"]');
container.setAttribute("data-namespace", route.namespace);
// Store references
this.currentNamespace = route.namespace;
Fungsi ini berjalan ketika pengguna mengklik link tersebut. Pada tahap ini, kami navigate() Metode menangani pertukaran halaman penuh sebaris – belum ada mesin konversi.
Mari kita lihat apa yang terjadi ketika pengguna mengklik link:
- Klausa penjaga memeriksa apakah kita sudah mengonversi atau apakah tautan yang diklik mengarah ke halaman saat ini.
- Kemudian kami memperbarui URL-nya.
- Kami mengurai rute dan mengimpor modul halaman secara dinamis. Lalu kita jalankan secara langsung
innerHTMLTukar — logika yang sama seperti di bawah iniloadInitialPage()tetapi dipicu oleh navigasi pengguna, bukan pemuatan awal.
async navigate(path) window.location.pathname === path) return;
// Update the URL in the address bar without triggering a page reload
window.history.pushState(, "", path);
// Resolve the matching route
const route = routes[path]
// Dynamically import the next page module
const pageModule = await route.loader();
// Swap the HTML content directly — no animation yet
const content = document.getElementById("page_content");
content.innerHTML = pageModule.default();
// Update the namespace
const container = document.querySelector('[data-transition="container"]');
container.setAttribute("data-namespace", route.namespace);
// Update internal state for the next navigation cycle
this.currentNamespace = route.namespace;
Pada langkah berikutnya, kita akan mengekstrak logika pertukaran ini ke dalam mesin transformasi khusus dan mengganti pertukaran inline dengan animasi performTransition() Memanggil – tapi pertama-tama, kita perlu memastikan pipanya kuat.
inisialisasi()
init() Lakukan tiga hal secara berurutan:
- Pertama, ia memuat halaman awal – URL apa pun yang digunakan pengguna. kita
awaitDengan cara ini, halaman pertama dirender sepenuhnya sebelum kita mulai mendengarkan klik. - Kemudian, ia mendaftarkan a
global click listeneradadocument. Daripada menambahkan pendengar acara ke setiap acara<a>tag (yang rusak ketika secara dinamis memasukkan tautan baru selama konversi), kami menggunakan delegasi peristiwa: itu muncul pada setiap klikdocumentlalu kita periksa apakah berasal dari tag jangkar menggunakanclosest("a"). - Kami memfilter tautan eksternal dengan memeriksa
startsWith(window.location.origin). - Kami memblokir navigasi default browser
e.preventDefault()dan gunakan metode kami untuk mencegah klik transisi perantaraisTransitioningKunci.
async init()
// Load and render whatever page matches the current URL
await this.loadInitialPage();
// Global click listener using event delegation
document.addEventListener("click", (e) => );
CATATAN: Ini adalah pengaturan yang sangat kecil – kami tidak menanganinya popstate acara belum.
Oke, mari kita periksa apakah mesin perutean dasar kita berfungsi dengan baik. Persetan denganmu main.js file, impor router, dan panggil init() Fungsi.
import "./style.css";
import router from "./router.js";
router.init();
Jalankan server lokal Anda npm run dev Kemudian klik tautan navigasi atas – Anda seharusnya dapat bernavigasi antar halaman dengan lancar.
Kami membuat kemajuan besar!
Langkah 4: Mesin Transisi
Untuk saat ini, kita dapat menyimpan file router (kita akan segera membahasnya kembali) dan membuat file /transitions folder di dalam /src.
Folder ini akan berisi:
- satu
/animationsFolder tersebut akan berisi file timeline animasi kami untuk transisi halaman selanjutnya. - satu
pageTransition.jsdokumen.
├── transitions/
│ ├── animations/
│ │ └── defaultTransition.js
│ └── pageTransitions.js
Sebelum membuat mesin transisi sederhana, mari buat garis waktu animasi untuk transisi tersebut.
kami akan menggunakan GSAP untuk animasi.
Memasang GSAP Gunakan perintah berikut: npm install gsap
Saya sangat merekomendasikan membuat /lib Folder tersebut berisi a index.js File yang berisi semua impor dan ekspor perpustakaan – Ini memberi Anda satu titik masuk untuk ketergantungan yang berat.
import gsap from "gsap";
export gsap ;
Sekarang mari kita menulis animasi transisi itu sendiri.
Efek yang kita inginkan: halaman saat ini bergeser sedikit ke atas dengan sedikit memudar, sedangkan halaman berikutnya secara bersamaan clip-path kartun. Keduanya terjadi secara bersamaan, sehingga menciptakan kesan berlapis-lapis.
Kiat utama: Kami mengatur wadah berikutnya ke position: fixed Jadi itu ditumpuk di atas halaman saat ini. dikombinasikan dengan inisial clip-path nyatakan, halaman berikutnya benar-benar tersembunyi – lalu kami menganimasikannya clip-path Untuk pengungkapan yang bersih.
Untuk efek fade-out yang keren, atur background-color milikmu body Elemen menjadi hitam.
Fungsi animasi menerima dua parameter: currentContainer Dan nextContainerDan Kembali ke garis waktu animasi.
import gsap from "../../lib/index.js";
export async function defaultTransition(currentContainer, nextContainer)
gsap.set(nextContainer,
clipPath: "inset(100% 0% 0% 0%)",
opacity: 1,
position: "fixed",
top: 0,
left: 0,
width: "100%",
height: "100vh",
zIndex: 10,
willChange: "transform, clip-path",
);
const tl = gsap.timeline();
tl.to(
currentContainer,
y: "-30vh",
opacity: 0.6,
force3D: true,
duration: 1,
ease: "power2.inOut",
,
0,
)
.fromTo(
nextContainer,
clipPath: "inset(100% 0% 0% 0%)" ,
clipPath: "inset(0% 0% 0% 0%)",
duration: 1,
force3D: true,
ease: "power2.inOut",
,
0,
);
return tl;
konversi halaman()
Mari kita kembali ke kita pageTransition.js dokumen.
Ini adalah inti dari pola asynchronous. Mesin membuat penampung kedua, memasukkan halaman berikutnya ke dalamnya, menjalankan animasi di antara dua penampung, lalu membersihkannya.
Pertama, impor GSAP instance dan fungsi animasi yang baru kami buat.
Fungsi ini hanya menerima nextHTML -Itu saja yang diperlukan. Mari kita uraikan prosesnya:
- Kami menanyakan kontainer saat ini dan pembungkus induknya.
- Lalu kami menelepon
cloneNode(false)– inifalseParameter berarti kita mengkloning elemen itu sendiri (tag yang sama, atribut yang sama) tetapi TIDAK anak-anaknya. - kita membuat sebuah
<main>elemen, masukkan HTML halaman berikutnya ke dalamnya, dan tambahkan ke wadah kloning kami. Lalu kita tempelkan wadah itu pada bungkusnya. - Kami meneruskan kedua kontainer ke
defaultTransition()yang mengembalikan aGSAP timeline. iniawait timeline.then()Baris tersebut menjeda eksekusi hingga masing-masing tween di timeline selesai. - Terakhir, kita bersihkan: hapus container lama dari DOM dan bersihkan semua gaya inline GSAP (
position: fixed,clip-path,opacityDLL. ).
import gsap from "../lib/index.js";
import defaultTransition from "./animations/defaultTransition.js";
export async function executeTransition( nextHTML )
const currentContainer = document.querySelector(
'[data-transition="container"]',
);
const wrapper = document.querySelector('[data-transition="wrapper"]');
// Clone the container structure for the next page
const nextContainer = currentContainer.cloneNode(false);
const content = document.createElement("main");
content.id = "page_content";
content.className = "page_content";
content.innerHTML = nextHTML;
nextContainer.appendChild(content);
// Append next page to DOM — both pages now coexist
wrapper.appendChild(nextContainer);
// Run the transition animation
const timeline = defaultTransition(currentContainer, nextContainer);
// Wait for animation to complete
await timeline.then();
// Cleanup: remove old page, reset transforms
currentContainer.remove();
gsap.set(nextContainer, clearProps: "all" );
gsap.set(nextContainer, force3D: true );
Saat transformasi berjalan, DOM kita akan terlihat seperti ini:
<div data-transition="wrapper">
<!-- Current page (animating out) -->
<div data-transition="container" data-namespace="home">
<main id="page_content" class="page_content">
<!-- home HTML -->
</main>
</div>
<!-- Next page (animating in) -->
<div data-transition="container" data-namespace="about">
<main id="page_content" class="page_content">
<!-- about HTML -->
</main>
</div>
</div>
Sekarang mari kita kembali ke kita router.js Arsipkan dan terapkan yang baru pageTransition() logika.
Mari buat metode khusus untuk ini.
Pertama, impor executeTransition() Fungsi yang baru saja kita buat.
Kami akan langsung menggantinya innerHTML menukarkan navigate() memiliki yang sesuai performTransition() metode dan tambahkan a popstate penangan.
melakukan konversi()
Fungsi ini mengambil satu parameter: path.
Proses terperinci:
- Blokir eksekusi jika
isTransitioningBenderanya aktif atau kita sudah berada di halaman yang sama. - Impor halaman berikutnya secara dinamis
module. - Jalankan transformasi asinkron menggunakan
executeTransition(). - perbarui saat ini
namespacedan mengatur ulangisTransitioningditandai sebagaifalsesekaliexecuteTransition()Jadwal selesai.
async performTransition(path)
// Block if a transition is already running
if (this.isTransitioning) return;
this.isTransitioning = true;
try finally
// Release the lock no matter what — even if an error occurs
this.isTransitioning = false;
navigate() Pembaruan URL kini hanya ditangani menggunakan pushState dan mendelegasikan segalanya kepada performTransition().
async navigate(path)
Acara status populer
Sekarang kita dapat menambahkan a dengan benar popstate penangan. Karena browser telah memperbarui URL popstate peristiwa terjadi, kita menyebutnya performTransition() Secara langsung – tidak pushState membutuhkan:
di dalam init() Fungsinya, tambahkan pendengar acara popstate peristiwa.
window.addEventListener("popstate", () =>
if (!this.isTransitioning)
this.performTransition(window.location.pathname);
);
Itu sebabnya kami berpisah navigate() Dan performTransition():
navigate() untuk navigasi terprogram (klik → pushState → transisi), sementara performTransition() Digunakan ketika URL telah berubah (popstate → transisi saja).
Catatan: Di pendengar klik, kami mengekstrak jalur dari jalur tautan href Penggunaan atribut new URL(link.href).pathnamepada saat yang sama popstate handler dari mana kita membacanya window.location.pathname. Hasil yang sama, sumber yang berbeda: ketika diklik, kita tahu ke mana pengguna ingin pergi dari tautan tersebut; popstatebrowser sudah mengupdate URL-nya, jadi kita tinggal membaca lokasi saat ini.
Selamat – mesin konversi Anda berfungsi!
Semuanya berfungsi dengan baik, tapi mari kita tambahkan animasi input yang sangat mendasar untuk memberikan gerakan yang lebih mendalam.
Langkah 5: Masuk ke animasi
Buat sebuah /animations folder di dalam src dan tambahkan a enter.js dokumen.
Ayo bernyawa <h1> dari positif y Offset kembali ke posisi semula.
Kami perlu menargetkan judul yang tepat karena keduanya <h1> Elemen tersebut ada saat transformasi sedang berjalan. Jadi fungsinya akan mengambil dua parameter: delay untuk menyempurnakan animasi, dan nextContainer.
import gsap from "../lib";
const ENTER = (nextContainer, delay) =>
const t = nextContainer?.querySelector("h1");
if (!t) return null;
gsap.set(t, y: "100%" );
const tl = gsap.timeline(
delay,
);
tl.to(
t,
y: 0,
duration: 1.2,
force3D: true,
ease: "expo.out",
,
0,
);
return timeline: tl ;
;
export default ENTER;
Panggil fungsi animasi ini di dalam init() Fungsionalitas modul dua halaman.
import template from "./about.html?raw";
import ENTER from "../../animations/Enter";
export default function AboutPage()
return template;
export function init( container )
ENTER(container, 0.45);
export function cleanup()
Sekarang kita perlu menelepon init() Pada saat yang sama kami menelepon executeTransition() di dalam performTransition().
Perbarui bagian ini:
await executeTransition(
nextHTML: pageModule.default(),
nextModule: pageModule,
);
Sekarang executeTransition() menerima nextModule dan menyebutnya init() Metode (jika ada):
if (nextModule?.init)
nextModule.init( container: nextContainer );
Terlihat jauh lebih baik sekarang!
Melangkah lebih jauh
Apa yang kami bangun adalah sistem yang berfungsi penuh namun minimal. Ini mencakup mekanisme inti—perutean, manajemen DOM kontainer ganda, dan transisi animasi—tetapi implementasi siap produksi perlu mengatasi beberapa aspek tambahan.
- Kait siklus hidup halaman.
- Batalkan transisi.
- Ambil terlebih dahulu saat mengarahkan kursor.
- Perbarui tag meta.
Daftarnya bisa terus bertambah…
Keindahan membangunnya dari awal adalah bahwa setiap bagian adalah sesuatu yang dapat Anda pahami, miliki, dan kembangkan.
Dengan beberapa penyesuaian dan eksperimen, Anda bisa mendapatkan hasil yang sangat keren:
rencana pengembangan website
metode pengembangan website
jelaskan beberapa rencana untuk pengembangan website, proses pengembangan website, kekuatan dan kelemahan bisnis pengembangan website
, jasa pengembangan website, tahap pengembangan website, biaya pengembangan website
#Membangun #transisi #halaman #asinkron #dalam #vanilla #JavaScript