Vanilla JS + Vite — router — history API bilan
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
/sozlamalarga 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]
pushStateo'zi hech narsa chizmaydi vapopstateni 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 dapublic/_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'zgartirishpopstate— "Orqaga"/"Oldinga";pushStateuni chaqirmaydi- Router: yo'l → regex → sahifa funksiyasi;
:idparametrlar - Havolalarni
clickdelegatsiyasi bilan ushlash; Ctrl+klik ni buzmaslik aria-current,document.title, fokus — qulaylik uchun- Production server barcha yo'llar uchun
index.htmlqaytarishi kerak
Keyingi dars
79-dars: Vanilla JS + Vite — state boshqarish — oddiy store pattern