Vanilla JS + Vite — router — history API bilan

~25 daq · JavaScript · Vanilla JS + Vite

Muammodan boshlaylik

Ilovada bir nechta "sahifa" kerak: bosh sahifa, vazifalar, sozlamalar. Eng oddiy yo'l — tugma bosilganda kontentni almashtirish:

tugma.onclick = () => (app.innerHTML = sozlamalarHtml);

Lekin:

  • Manzil o'zgarmaydi — sahifani ulashib bo'lmaydi
  • F5 — yana bosh sahifa
  • Brauzerning "Orqaga" tugmasi saytdan chiqarib yuboradi

Kerak: manzil o'zgarsin, lekin sahifa qayta yuklanmasin.


History API

history.pushState(holat, '', '/sozlamalar');
  • Manzil satri /sozlamalar ga o'zgaradi
  • Sahifa qayta yuklanmaydi
  • Tarixga yangi yozuv qo'shiladi
Metod/hodisa Vazifasi
history.pushState(state, '', url) Yangi yozuv qo'shish
history.replaceState(state, '', url) Joriy yozuvni almashtirish
history.back() / forward() Orqaga / oldinga
popstate hodisasi Foydalanuvchi "Orqaga"/"Oldinga" bosganda
location.pathname Joriy yo'l

[!INFO] pushState o'zi hech narsa chizmaydi va popstate ni chaqirmaydi — u faqat manzilni o'zgartiradi. Kontentni yangilash — sizning vazifangiz.


Oddiy router

// src/router.js
const marshrutlar = [];
let ildiz;

export function marshrut(yol, sahifa) {
  // '/vazifa/:id' → /^\/vazifa\/([^/]+)$/
  const kalitlar = [];
  const regex = new RegExp(
    '^' + yol.replace(/:(\w+)/g, (_, k) => (kalitlar.push(k), '([^/]+)')) + '
#39;, ); marshrutlar.push({ regex, kalitlar, sahifa }); } async function chiz() { const yol = location.pathname; for (const { regex, kalitlar, sahifa } of marshrutlar) { const moslik = yol.match(regex); if (moslik) { const params = Object.fromEntries( kalitlar.map((k, i) => [k, decodeURIComponent(moslik[i + 1])]), ); ildiz.replaceChildren(await sahifa(params)); return; } } ildiz.textContent = 'Sahifa topilmadi'; } export function navigate(yol) { if (yol === location.pathname) return; history.pushState(null, '', yol); chiz(); } export function boshla(element) { ildiz = element; // "Orqaga" / "Oldinga" window.addEventListener('popstate', chiz); // ichki havolalarni ushlash document.addEventListener('click', (e) => { const a = e.target.closest('a[data-link]'); if (!a || e.ctrlKey || e.metaKey || e.shiftKey || e.button !== 0) return; e.preventDefault(); navigate(a.getAttribute('href')); }); chiz(); }

Ishlatish

// src/main.js
import { marshrut, boshla } from './router.js';

marshrut('/', () => {
  const div = document.createElement('div');
  div.innerHTML = '<h1>Bosh sahifa</h1>';
  return div;
});

marshrut('/vazifa/:id', ({ id }) => {
  const div = document.createElement('div');
  const h1 = document.createElement('h1');
  h1.textContent = `Vazifa #${id}`;
  div.append(h1);
  return div;
});

marshrut('/sozlamalar', async () => {
  const { SozlamalarSahifasi } = await import('./pages/sozlamalar.js');
  return SozlamalarSahifasi();
});

boshla(document.querySelector('#sahifa'));
<nav>
  <a href="/" data-link>Bosh</a>
  <a href="/vazifa/42" data-link>42-vazifa</a>
  <a href="/sozlamalar" data-link>Sozlamalar</a>
</nav>
<main id="sahifa"></main>
  • Havolalar — oddiy <a href>: JavaScript ishlamasa ham, yangi tabda ochish (Ctrl+klik) ham ishlaydi
  • data-link — router ushlaydigan ichki havolalar
  • /sozlamalar — dinamik import, alohida chunk (76-dars)

Qo'shimchalar

Faol havolani belgilash

function faolniBelgila() {
  document.querySelectorAll('a[data-link]').forEach((a) => {
    const faol = a.getAttribute('href') === location.pathname;
    if (faol) a.setAttribute('aria-current', 'page');
    else a.removeAttribute('aria-current');
  });
}

chiz() oxirida chaqiring. CSS: a[aria-current="page"] { font-weight: 700; }.

Sarlavha va fokus

document.title = `Sozlamalar — Vazifalar`;
ildiz.querySelector('h1')?.focus();   // h1 ga tabindex="-1" bering
window.scrollTo(0, 0);

Ekran o'quvchi foydalanuvchilar sahifa almashganini bilishi uchun fokusni yangi sarlavhaga o'tkazing.

Query parametrlar

const params = new URLSearchParams(location.search);
const qidiruv = params.get('q') ?? '';

Server sozlamasi

/vazifa/42 da F5 bossangiz — server vazifa/42 faylini qidiradi. Vite dev serveri buni o'zi hal qiladi, production da esa barcha yo'llar uchun index.html qaytarish kerak:

location / {
  try_files $uri $uri/ /index.html;
}

[!WARNING] Vercel da vercel.json (rewrites), Netlify da public/_redirects (/* /index.html 200). Bu sozlama bo'lmasa, foydalanuvchi ichki sahifani yangilaganda 404 oladi.

[!TIP] Yangi brauzerlarda Navigation API (navigation.addEventListener('navigate', ...)) mavjud — u havolalarni ushlash va tarixni boshqarishni soddalashtiradi. Barcha brauzerlarda qo'llab-quvvatlanguncha History API — ishonchli tanlov.


Nima uchun shunday?

Nima uchun SPA ga manzil kerak — kontent JavaScript da chizilsa yetmaydimi?

Chunki URL — vebning asosiy va'dasi: har bir sahifaning o'z manzili bor, uni xatcho'pga qo'shish, ulashish, qidiruv tizimida topish va "Orqaga" bilan qaytish mumkin. Manzilsiz SPA bu va'dani buzadi va foydalanuvchiga g'alati tuyuladi. History API sahifani qayta yuklamasdan manzilni boshqarish imkonini beradi — SPA ning tezligi va oddiy saytning qulayligi birlashadi. React Router va boshqa router kutubxonalari ichida aynan shu mexanizm ishlaydi.


Xulosa

Eslab qoling:

  • history.pushState(null, '', yol) — manzilni sahifa yangilanmasdan o'zgartirish
  • popstate — "Orqaga"/"Oldinga"; pushState uni chaqirmaydi
  • Router: yo'l → regex → sahifa funksiyasi; :id parametrlar
  • Havolalarni click delegatsiyasi bilan ushlash; Ctrl+klik ni buzmaslik
  • aria-current, document.title, fokus — qulaylik uchun
  • Production server barcha yo'llar uchun index.html qaytarishi kerak

Keyingi dars

79-dars: Vanilla JS + Vite — state boshqarish — oddiy store pattern