Membangun transisi halaman asinkron dalam vanilla JavaScript

 – Beragampengetahuan
17 mins read

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:

  • currentNamespace Melacak halaman yang sedang ditampilkan, memungkinkan kita melewati navigasi ke halaman yang sama.
  • isTransitioning Merupakan 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 routes Array untuk memperlambat memuat modul yang benar.
  • Memuat modul.
  • Suntikkan HTML-nya ke dalam #page_content Elemen tersebut sudah ada di shell HTML kita.
  • mendirikan namespace Lacak 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;

navigasi()

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 innerHTML Tukar — logika yang sama seperti di bawah ini loadInitialPage()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 await Dengan cara ini, halaman pertama dirender sepenuhnya sebelum kita mulai mendengarkan klik.
  • Kemudian, ia mendaftarkan a global click listener ada document. 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 klik documentlalu kita periksa apakah berasal dari tag jangkar menggunakan closest("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 perantara isTransitioning Kunci.
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 /animations Folder tersebut akan berisi file timeline animasi kami untuk transisi halaman selanjutnya.
  • satu pageTransition.js dokumen.
├── 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) – ini false Parameter 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 a GSAP timeline. ini await 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 isTransitioning Benderanya aktif atau kita sudah berada di halaman yang sama.
  • Impor halaman berikutnya secara dinamis module.
  • Jalankan transformasi asinkron menggunakan executeTransition().
  • perbarui saat ini namespace dan mengatur ulang isTransitioning ditandai sebagai false sekali executeTransition() 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;
  

perbarui navigasi()

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

Tinggalkan Balasan

Alamat email Anda tidak akan dipublikasikan. Ruas yang wajib ditandai *