IlmHamroh
JavaScript Full-stack/19-qism. React: routing, server state, formalar1/36-dars24 daqiqa
Mundarija (35)

SPA routing qanday ishlaydi: History API, server zaxirasi va hash routing

Qisqacha: Bitta sahifali ilovada (SPA) havola bosilganda sahifa serverdan qayta yuklanmaydi. Router history.pushState bilan manzilni o'zgartiradi va kerakli sahifa komponentini o'zi chizadi, «Orqaga» bosilsa — popstate hodisasini tinglaydi. Lekin sahifa yangilansa (F5) yoki havola tashqaridan ochilsa, so'rov serverga boradi. Server har noma'lum yo'lga index.html ni qaytarishi kerak — bu SPA zaxirasi (SPA fallback). Ilova fayllarining yo'llari esa absolyut bo'lishi shart.

Bu darsda

  • Server routing va client routing farqini bitta so'rovlar sxemasida ko'rasiz.
  • Router'ning uch vazifasini (manzil → sahifa, havola, «Orqaga») React'da o'zingiz yozasiz.
  • Sahifa yangilanganda nima bo'lishini va SPA zaxirasini turli hostinglarda sozlay olasiz.
  • Nisbiy yo'l (./assets/) chuqur havolada nega buzilishini new URL bilan isbotlaysiz.
  • Hash routing va History API'ni tanlash mezonlarini bilasiz; vazifalar 19-qismga tayyorlanadi.

Oldin bilishingiz kerak: location va History API: oddiy SPA router, State'ni ko'tarish, useSyncExternalStore, React'da auth holati.

1. Nega bu kerak?

18-qism auth darsi bilan tugadi: vazifalar v8 — kirish formasi va bitta katta sahifa. Ro'yxat, filtrlar, statistika paneli, tema tanlovi — hammasi bir joyda. Telefonda bu uzun lenta bo'lib qoldi.

Jasur aka ikki narsa so'radi. Birinchisi: statistika alohida sahifada bo'lsin. Ikkinchisi: Sardorga Telegram'da havola yuborsa, u to'g'ridan-to'g'ri statistikani ochsin — …/vazifalar/statistika. Buning uchun ilova sahifalarga bo'linadi va har sahifaning o'z manzili bo'ladi.

History API darsida siz pushState va popstate bilan oddiy router yozgansiz. useSyncExternalStore darsida URL'dagi ?filtr= ni React'ga ulagansiz. Bu dars ikkalasini bitta rasmga yig'adi. Keyingi darsdan boshlab tayyor kutubxona — React Router — bilan ishlaymiz. Uni to'g'ri ishlatish uchun esa uning ichida va server tomonida nima bo'lishini bilish kerak.

Hayotiy o'xshatish: teatr. Oddiy sayt — har sahna uchun tomoshabinni boshqa binoga ko'chirish: kiyinish, yo'l, yangi o'rindiq. SPA — bitta zal. Parda yopiladi, dekoratsiya almashadi, tomoshabin joyidan turmaydi. Lekin kimdir "3-sahnaga chipta" bilan to'g'ridan-to'g'ri kelsa, kassa uni baribir shu zalga yo'llashi kerak. Bu dars ana shu kassa haqida ham.

2. Ikki xil routing

2.1 Server routing: har bosish — yangi sahifa

Routing (yo'naltirish) — manzilga qarab qaysi sahifani ko'rsatishni hal qilish. HTML darslaridagi oddiy saytda buni server qiladi. Siz menyu.html havolasini bosasiz. Brauzer serverdan menyu.html ni so'raydi, eski sahifani butunlay tashlaydi va yangisini noldan quradi. Bu server routing (server tomonida yo'naltirish).

Bunda JavaScript holati har o'tishda yo'qoladi. Formaga yozilgan matn, ochiq menyu, xotiradagi ro'yxat — hammasi. CSS va JS fayllari keshdan bo'lsa ham qayta bajariladi, ekran bir lahza oqarib ketadi.

2.2 Client routing: sahifa joyida, manzil o'zgaradi

Client routing (mijoz tomonida yo'naltirish) — yo'naltirishni brauzerdagi JavaScript qiladi. Server faqat bir marta, boshida, index.html va JS faylini beradi. Keyin havola bosilganda:

  1. JS standart o'tishni to'xtatadi (preventDefault);
  2. manzil satrini history.pushState bilan o'zgartiradi — so'rov yo'q;
  3. yangi manzilga mos komponentni chizadi.

Shunday ishlaydigan ilova — SPA (Single Page Application, bitta sahifali ilova). Bu kodni yozadigan qism — router. Manzil va unga mos sahifa juftligi esa marshrut (route) deyiladi: /statistika → StatistikaSahifasi.

Ikki yo'lni yonma-yon ko'ring. Yuqorida — har bosishda server, pastda — server faqat birinchi marta:

sequenceDiagram
  participant B as Brauzer
  participant R as Router (JS)
  participant S as Server
  Note over B,S: Server routing
  B->>S: GET /statistika.html
  S-->>B: yangi HTML, sahifa noldan
  Note over B,S: Client routing
  B->>S: GET / (faqat birinchi marta)
  S-->>B: index.html + JS
  B->>R: havola bosildi
  R->>B: pushState("/statistika")
  R->>B: Statistika komponentini chiz

Diagrammaning pastki qismida R dan keyin S ga o'q yo'q. Sahifa almashdi, server esa bundan bexabar.

Server routing Client routing (SPA)
Kim sahifa tanlaydi server brauzerdagi router
O'tishda tarmoq yangi HTML ko'pincha hech narsa
JS holati har o'tishda yo'qoladi saqlanadi
Birinchi ochilish darhol HTML JS yuklanib, chizilguncha

Oxirgi qatorga e'tibor bering. SPA'ning ham narxi bor: birinchi ochilishda sahifa bo'sh <div>, mazmunni JS chizadi. Bunga «Real ishda» bo'limida qaytamiz.

Tekshirib ko'ring: SPA'da «Statistika» havolasini bosdingiz, keyin brauzerning «Orqaga» tugmasini. Shu ikki harakatda serverga nechta sahifa so'rovi ketdi?

Javob

Nolta. Havola bosilganda router pushState qildi — bu so'rov emas. «Orqaga» bosilganda brauzer popstate hodisasini chiqardi, router eski sahifani o'zi chizdi. Sahifa ma'lumot so'rashi mumkin (API'dan) — lekin bu HTML sahifa so'rovi emas.

3. Router'ning uch vazifasi

Har qanday router — o'zingiz yozgani ham, React Router ham — uchta ishni bajaradi:

Vazifa Brauzerda nima bilan
Manzilga qarab sahifa tanlash location.pathname → komponent
Havola bosilganda o'tish preventDefault + pushState + qayta chizish
«Orqaga» va «Oldinga» popstate hodisasi → qayta chizish

Keling, uchalasini React'da yozamiz. Lekin avval bitta to'siq.

3.1 Natija oynasida manzil satri yo'q

Saytdagi «Ishga tushir» kodni alohida natija oynasida bajaradi. U — sandbox'li iframe, manzili about:srcdoc. Unda haqiqiy manzil satri yo'q. History API darsida ko'rgansiz: bunday hujjatda pushState SecurityError tashlaydi.

Shuning uchun jonli misolda tarixni xotirada saqlaymiz: manzillar massivi va "hozir qaysi birida turibmiz" raqami. Brauzer tarixi ham ichkarida aynan shunday tuzilgan. React Router'da ham shunday router bor — createMemoryRouter, keyingi darsda natija oynasida shuni ishlatamiz. Loyihada esa haqiqiy history.pushState ishlaydi — kodi shu bo'lim oxirida.

3.2 Uch vazifa — bitta komponentda

«Bahor» saytining uch sahifasi: bosh sahifa, menyu va aloqa. Mehmon «Menyu» ni, keyin «Aloqa» ni bosadi va «Orqaga» qaytadi:

jsx
import { useState } from "react";

// Marshrutlar: manzil → sahifa komponenti
const routes = {
  "/": () => <h1>Bahor oshxonasi</h1>,
  "/menyu": () => <h1>Menyu: osh, lag'mon, manti</h1>,
  "/aloqa": () => <h1>Tel: +998 90 000 00 00</h1>,
};

function NotFound() {
  return <h1>Sahifa topilmadi</h1>;
}

function Link({ to, onNavigate, children }) {
  function handleClick(event) {
    // Ctrl/Cmd/Shift + bosish — yangi tab: brauzerning o'zi qilsin
    if (event.ctrlKey || event.metaKey || event.shiftKey) return;
    event.preventDefault(); // sahifa serverdan yuklanmasin
    onNavigate(to);
  }
  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}

function App() {
  // Tarix: manzillar ro'yxati va hozir qaysi birida turganimiz
  const [memory, setMemory] = useState({
    entries: ["/"],
    index: 0,
  });
  const path = memory.entries[memory.index];

  function navigate(to) {
    // pushState kabi: joriydan keyingilarni tashlab, yangisini qo'sh
    setMemory((m) => ({
      entries: [...m.entries.slice(0, m.index + 1), to],
      index: m.index + 1,
    }));
  }

  function goBack() {
    // «Orqaga» kabi: ro'yxat o'sha, faqat ko'rsatkich bitta orqaga
    setMemory((m) => ({ ...m, index: Math.max(0, m.index - 1) }));
  }

  console.log("manzil:", path, "| tarix:", memory.entries.join(" "));
  const Page = routes[path] ?? NotFound;
  return (
    <div>
      <nav>
        <Link to="/" onNavigate={navigate}>Bosh sahifa</Link>
        {" | "}
        <Link to="/menyu" onNavigate={navigate}>Menyu</Link>
        {" | "}
        <Link to="/aloqa" onNavigate={navigate}>Aloqa</Link>
      </nav>
      <button onClick={goBack}>Orqaga</button>
      <Page />
    </div>
  );
}

Konsolda:

text
manzil: / | tarix: /
manzil: /menyu | tarix: / /menyu
manzil: /aloqa | tarix: / /menyu /aloqa
manzil: /menyu | tarix: / /menyu /aloqa

Ekranda:

text
Bosh sahifa | Menyu | Aloqa
Orqaga
Menyu: osh, lag'mon, manti

Konsolni qatorma-qator o'qiymiz:

Qadam entries index Ekranda
boshlanish / 0 Bahor oshxonasi
«Menyu» /, /menyu 1 Menyu
«Aloqa» /, /menyu, /aloqa 2 Aloqa
«Orqaga» o'sha uchta 1 Menyu

To'rtinchi qatorga qarang: «Orqaga» tarixdan hech narsani o'chirmadi. /aloqa ro'yxatda qoldi — «Oldinga» uchun. Faqat ko'rsatkich siljidi. Endi shu holatda yangi havola bosilsa, slice(0, index + 1) /aloqa ni kesib tashlaydi. Brauzer tarixi ham aynan shunday: orqaga qaytib yangi sahifaga o'tsangiz, «Oldinga» o'chadi.

Uch vazifa kodda qayerda:

  • Manzil → sahifa: routes[path] ?? NotFound. Mos marshrut yo'q bo'lsa — "topilmadi" sahifasi. Bu ?? (Nullish coalescing) bo'lmasa, /yoq manzilida Page undefined bo'lib, React yiqilardi.
  • Havola: Link — oddiy <a href>, lekin bosilganda preventDefault va navigate. navigate — pushState ning o'rnini bosadi.
  • «Orqaga»: goBack — popstate ning o'rnini bosadi. Haqiqiy brauzerda bu tugma sizning kodingizda emas, brauzerning o'zida.

Manzil — state. U o'zgarsa, React App ni qayta chizadi va yangi sahifa chiqadi. UI = f(state) g'oyasi routing'da ham o'sha: sahifa — manzilning funksiyasi.

React Router'ning createMemoryRouter ichidagi tarix ham xuddi shu ikki maydondan iborat. react-router 8.4.0 manba kodida (createMemoryHistory) push shunday: index += 1; entries.splice(index, entries.length, nextLocation). Biz yozgan slice bilan bir xil ish.

3.3 Nega <a href>, tugma emas?

Link ichida <button> ham ishlardi. Lekin havola — havola bo'lib qolishi kerak:

  • Ctrl + bosish yoki sichqonchaning o'rta tugmasi — yangi tabda ochish. Buni href beradi. Link dagi birinchi tekshiruv aynan shuning uchun: Ctrl bosilgan bo'lsa, biz aralashmaymiz.
  • O'ng tugma → "Havolani nusxalash" — Sardorga Telegram'da yuborish uchun.
  • Malika (ekran o'quvchi) "havola, Menyu" deb eshitadi va havolalar ro'yxatidan topadi.
  • JS hali yuklanmagan bo'lsa ham href ishlaydi — oddiy sahifa yuklanishi bilan.

React Router'ning <Link> i ham <a href> chizadi va xuddi shu tekshiruvlarni qiladi. Uchinchi mashqda shu tekshiruvni o'zingiz yozasiz.

3.4 Loyihada: haqiqiy pushState

Manzil satri bor haqiqiy sahifada tarixni brauzer saqlaydi. Manzil — React'dan tashqaridagi qiymat, demak unga useSyncExternalStore bilan obuna bo'linadi. Bu kod natija oynasida ishlamaydi (pushState — SecurityError), shuning uchun tugmasiz:

jsx
import { useSyncExternalStore } from "react";

const listeners = new Set();

function subscribe(listener) {
  listeners.add(listener);
  addEventListener("popstate", listener); // «Orqaga» / «Oldinga»
  return () => {
    listeners.delete(listener);
    removeEventListener("popstate", listener);
  };
}

export function navigate(to) {
  history.pushState(null, "", to);
  // pushState hodisa bermaydi — tinglovchilarga o'zimiz aytamiz
  for (const listener of listeners) listener();
}

export function usePathname() {
  return useSyncExternalStore(subscribe, () => location.pathname);
}

Jonli misoldagi memory o'rnida endi usePathname(), goBack esa umuman kerak emas — brauzerning tugmasi popstate beradi. Qolgani o'sha.

react-router (8.4.0) dagi createBrowserRouter ham shu ikki qismga qurilgan. Manba kodida globalHistory.pushState(historyState, "", url) va popstate tinglovchisi bor. Farqi — history.state ga null emas, { usr, key, idx } yozadi. idx — tarixdagi tartib raqami. Router u bilan «Orqaga» necha qadam bo'lganini hisoblaydi. Shu sababli tarixga router'dan tashqarida, o'zingiz pushState(null) yozmang. vazifalar 17-qismdan beri aynan shunday qiladi — buni URL — holat joyi darsida o'lchab tuzatamiz.

Tekshirib ko'ring: Jonli misolda «Menyu» → «Aloqa» → «Orqaga» → «Bosh sahifa» bosildi. memory.entries da nima qoladi?

Javob

["/", "/menyu", "/"], index: 2. «Orqaga» dan keyin index 1 edi (/menyu). «Bosh sahifa» slice(0, 2) bilan /aloqa ni kesdi va / ni qo'shdi. «Oldinga» endi hech qayerga olib bormaydi — brauzerda ham shunday.

4. Sahifa yangilansa: endi server ishga tushadi

4.1 Chuqur havola

Client routing faqat sahifa ochiq paytda ishlaydi. Uch holatda brauzer baribir serverga boradi:

  • sahifa yangilandi (F5);
  • havola Telegram'dan, xatchodan yoki xatdan ochildi;
  • manzil satriga qo'lda yozildi.

Ilovaning ichki sahifasiga to'g'ridan-to'g'ri olib boradigan bunday havola — chuqur havola (deep link). Server /vazifalar/statistika so'rovini oladi va diskdan shu nomli faylni qidiradi. Bunday fayl yo'q: diskda faqat index.html va assets/ bor. Server 404 qaytaradi. Router hech qachon ishga tushmaydi, chunki uning JS fayli ham kelmadi.

Yechim: "fayl topilmasa — index.html ni ber". Bu — SPA zaxirasi (SPA fallback). Shunda index.html keladi, JS yuklanadi, router location.pathname ni o'qiydi va statistikani chizadi:

flowchart TD
  A["F5: /vazifalar/statistika"] --> B{"Shu nomli fayl bormi?"}
  B -->|"ha"| C["Faylni beradi"]
  B -->|"yo'q"| D{"Zaxira sozlanganmi?"}
  D -->|"yo'q"| E["404: ilova yo'q"]
  D -->|"ha"| F["index.html"]
  F --> G["JS yuklanadi"]
  G --> H["Router manzilni o'qiydi"]
  H --> I["Statistika chiziladi"]

Diagrammada sahifani tanlaydigan oxirgi qadam yana brauzerda. Server sahifani "tushunmaydi" — u faqat hamma yo'lga bitta faylni beradi.

4.2 Zaxira turli hostinglarda

Joy Qanday sozlanadi HTTP holati
npm run dev, preview (Vite) o'zi qiladi 200
Vercel vercel.json → rewrites 200
Netlify _redirects: /* /index.html 200 200
Nginx try_files $uri /index.html; 200
GitHub Pages sozlama yo'q → 404.html hiylasi 404

vazifalar ning vercel.json ida (Vercel deploy darsidan beri) shu qator bor:

json
"rewrites": [{ "source": "/(.*)", "destination": "/index.html" }]

Rewrite (ichki almashtirish) — server foydalanuvchiga boshqa manzilni ko'rsatmay, ichkarida boshqa faylni beradi. Manzil satrida /statistika qoladi, javob — index.html mazmuni, holati 200. Yo'naltirish (redirect) esa manzilning o'zini o'zgartirardi — bu bizga kerak emas.

Bir nozik joy: Vercel bu qoidani faqat haqiqiy fayl topilmaganda qo'llaydi. /assets/index-….js — mavjud fayl, o'zi beriladi. Aks holda JS so'roviga ham HTML kelardi.

4.3 GitHub Pages: 404.html hiylasi va uning narxi

vazifalar ning asosiy manzili — GitHub Pages (17-qismdan). Pages'da rewrite yo'q. Lekin bitta qoida bor: topilmagan sahifa uchun sayt papkasidagi 404.html ni ko'rsatadi. Agar 404.html — index.html ning aynan nusxasi bo'lsa? Unda topilmagan har manzilda ilovaning o'zi ochiladi va sahifani router tanlaydi.

Bu ishlaydi. Lekin halol bo'laylik: server javobining holati baribir 404. Biz buni o'lchadik — kanon o'lchovi natijalar/02-chuqur-havola-v9.txt, Chrome 155, Pages kabi server (keyingi darsdagi holat):

text
/vazifalar/ → HTTP 200 | #ilova: 96 element | h2: "Ro'yxat" | title: "(2) Vazifalar — kichik vazifalar ro'yxati"
/vazifalar/statistika → HTTP 404 | #ilova: 32 element | h2: "Statistika" | title: "Statistika — Vazifalar"
   konsol: Failed to load resource: the server responded with a status of 404 (Not Found)

Sahifa ochildi, sarlavha "Statistika" — foydalanuvchi farqni sezmaydi. Narxi ikkita:

  1. Konsolda xato: Failed to load resource: the server responded with a status of 404 (Not Found) — "Resursni yuklab bo'lmadi: server 404 (Topilmadi) holati bilan javob berdi". Brauzer sahifa so'rovining holatini ko'rdi va yozdi. Ilova uchun zararsiz, lekin DevTools'ni ochgan odamni chalg'itadi.
  2. Qidiruv tizimi 404 ni "bu sahifa yo'q" deb tushunadi va indekslamaydi. Kirish talab qiladigan vazifalar uchun bu muammo emas. Ochiq blog yoki do'kon uchun — muammo. Ular Vercel/Netlify'ni yoki server rendering'ni tanlaydi.

Kanon buni yashirmaydi: TEXNIK-QARZ.md da 48-band. Vercel'da o'sha manzil 200 bilan ochiladi — vercel.json tayyor.

Tekshirib ko'ring: Nega 404.html ni index.html dan qo'lda nusxa qilib, repoga qo'yib qo'yish yomon fikr?

Javob

index.html har build'da o'zgaradi: unga hash'li fayl nomlari yoziladi (index-PzJwsspb.js). Qo'lda olingan nusxa bir build'dan keyin eskiradi va mavjud bo'lmagan JS faylni so'raydi — chuqur havola yana bo'sh ekran beradi. Shuning uchun kanonda nusxani build'ning o'zi oladi («Vazifalar qadami» da).

5. Nisbiy yo'l tuzog'i

5.1 Brauzer yo'lni qanday hisoblaydi

Zaxira bor, index.html keldi. Endi brauzer undagi fayllarni so'raydi. 18-qismgacha vazifalar da yo'llar nisbiy edi: Vite base: "./" bilan ./assets/index-….js deb yozardi. Nega shunday edi? Sayt login.github.io/vazifalar/ papkasida, papka nomi repoga qarab o'zgaradi. Nisbiy yo'l har papkada ishlardi.

Nisbiy yo'l joriy sahifa manziliga nisbatan hisoblanadi. Buni Node'da URL bilan tekshiramiz — brauzer xuddi shu algoritmni ishlatadi (URL va URLSearchParams):

js
const file = "./assets/index.js";
const site = "https://example.com";

// sahifa: /vazifalar/ (ro'yxat)
const home = new URL(file, site + "/vazifalar/");
console.log(home.pathname); // /vazifalar/assets/index.js

// sahifa: /vazifalar/vazifa/2 (chuqur havola)
const deep = new URL(file, site + "/vazifalar/vazifa/2");
console.log(deep.pathname); // /vazifalar/vazifa/assets/index.js

Ikkinchi natija — muammo. /vazifalar/vazifa/2 sahifasi uchun "joriy papka" — /vazifalar/vazifa/. Brauzer JS faylni o'sha yerdan qidiradi. U yerda fayl yo'q — server yana 404.html ni beradi, HTML'ni JS sifatida bajarib bo'lmaydi.

Bitta darajali manzil (/vazifalar/statistika) da esa joriy papka /vazifalar/ — fayllar topiladi. Shuning uchun tuzoq ikki bo'lakli manzilda birinchi marta chiqadi. Bizda bu — dinamik marshrutlar darsidagi /vazifa/2.

5.2 O'lchov: v8 + 404.html

Biz v8 build'iga 404.html nusxasini qo'yib ko'rdik, base: "./" esa o'zgarmadi (natijalar/01-chuqur-havola-v8.txt):

text
/vazifalar/vazifa/2 → HTTP 404 | #ilova: 0 element | h2: null | title: "Vazifalar — kichik vazifalar ro'yxati"
   yuklanmadi (4): 404 /vazifalar/vazifa/assets/js/tema.js, 404 /vazifalar/vazifa/assets/index-BUB1eWti.js, 404 /vazifalar/vazifa/assets/index-QnSPLFv3.css, 404 /vazifalar/vazifa/assets/ikonlar/ikon.svg
/vazifalar/statistika → HTTP 404 | #ilova: 88 element | h2: "Ro'yxat" | title: "(2) Vazifalar — kichik vazifalar ro'yxati"

Uch narsani ko'ring:

  1. vazifa/2 da to'rtta fayl topilmadi — hammasi /vazifalar/vazifa/assets/... dan so'ralgan. #ilova da 0 element: oq ekran.
  2. title bor — u HTML'ning o'zida yozilgan, JS kerak emas.
  3. statistika da fayllar yuklandi, lekin ekranda — "Ro'yxat". Hali router yo'q, v8 har manzilda bitta sahifani chizadi.

Yechim — absolyut yo'l: /vazifalar/assets/index-….js. U qaysi sahifadan so'ralsa ham bir joyga olib boradi. Papka nomi build vaqtida beriladi — «Vazifalar qadami» da.

Maslahat: Chuqur havolani doim ikki bo'lakli manzil bilan sinang (/vazifa/2, /menyu/osh). Bir bo'lakli manzil nisbiy yo'l xatosini yashiradi.

6. Hash routing

6.1 # dan keyingi qism serverga bormaydi

Zaxirasiz ishlaydigan yo'l ham bor. History API darsida ko'rgansiz: manzilning # dan keyingi qismi serverga yuborilmaydi. …/vazifalar/#/statistika ochilganda server faqat /vazifalar/ ni ko'radi — bu mavjud papka, 200. Sahifani router location.hash dan tanlaydi, o'tishni esa hashchange hodisasidan biladi:

js
// Hash router'ning yuragi: #/statistika → "/statistika"
function currentPath() {
  return location.hash.slice(1) || "/";
}
addEventListener("hashchange", () => render(currentPath()));

Bu — hash routing. React Router'da ham bor (createHashRouter). GitHub Pages'da zaxira kerak emas, status — 200.

6.2 Nega kanon hash'ni tanlamadi

History API (/statistika) Hash (#/statistika)
Server sozlamasi zaxira kerak kerak emas
Manzil ko'rinishi toza #/ bilan
# ning o'z vazifasi bo'sh — #royxat ishlaydi router egallaydi
Server sahifa yo'lini ko'radimi ha (SSR, statistika) yo'q

vazifalar uchun hal qiluvchi sabablar ikkita. Birinchisi — mavjud havolalar. History API darsidan beri filtr ?filtr=faol da, odamlar bunday havolalarni saqlab qo'ygan. Hash'ga o'tish ularning hammasini o'zgartirardi. Ikkinchisi — # band. Skip link href="#royxat" (Fokus boshqaruvi) va sahifa ichidagi langarlar # ning odatiy vazifasi.

Narxi — Pages'da 404 holati. Kanon uni qabul qildi va ochiq yozdi. Shunday qarorlarni o'lchov bilan qabul qilish — kursning qoidasi.

Tekshirib ko'ring: Hash routing'da sahifa yangilansa (F5), serverga qaysi manzil boradi: /vazifalar/#/statistika yoki /vazifalar/?

Javob

/vazifalar/. Brauzer # dan keyingi qismni so'rovga qo'shmaydi. Server mavjud papkani beradi (200), JS yuklangach router location.hash dan /statistika ni o'qiydi. Shuning uchun hash routing'ga zaxira kerak emas.

7. Service Worker: oflayn chuqur havola

vazifalar internetsiz ham ochiladi — 13-qismdagi Service Worker sahifani keshdan beradi. Lekin v8 dagi SW faqat /vazifalar/ yoki /vazifalar/index.html ni "ilova sahifasi" deb bilardi. Oflayn holda /vazifalar/statistika ochilsa, SW unga aralashmasdi — brauzerning "Internet yo'q" sahifasi chiqardi.

SW uchun ham qoida server bilan bir xil: ilovaning har sahifasi — o'sha bitta index.html. Faqat endi "sahifa"ni qanday tanish kerak? Kanondagi javob: kengaytmasi yo'q yo'l — sahifa (/statistika, /vazifa/2); kengaytmalisi (README.md, manifest.webmanifest) — fayl, .html dan tashqari. Kod — «Vazifalar qadami» da.

8. Ko'p uchraydigan xatolar

Belgi Sabab Davosi
F5 dan keyin 404 serverda SPA zaxirasi yo'q rewrite yoki 404.html nusxasi
chuqur havolada oq ekran, Network'da …/vazifa/assets/… 404 nisbiy yo'llar (./assets/) absolyut base
JS so'roviga HTML keladi: Unexpected token '<' rewrite hamma narsani index.html ga burgan faqat topilmagan yo'lga zaxira
havola bosilganda sahifa oqarib, qayta yuklanadi <a href> preventDefault siz router'ning Link i
«Orqaga» bosilsa manzil o'zgaradi, ekran — yo'q popstate tinglanmagan popstate → qayta chizish
Ctrl + bosish yangi tab ochmaydi hamma bosish ushlangan modifikator tugmalarni o'tkazib yuborish
SecurityError: Failed to execute 'pushState' boshqa origin'ga yozish yoki natija oynasi o'z origin'idagi yo'l; natija oynasida — xotiradagi tarix

Uchinchi qatordagi xabar — SyntaxError: Unexpected token '<' ("kutilmagan belgi <"). Brauzer JS kutgan joyda <!doctype html> ni o'qib, birinchi belgidayoq to'xtaydi. Xabar < haqida, chunki HTML shu belgi bilan boshlanadi — JS kodi emas.

9. Vazifalar qadami: chuqur havolaga tayyorgarlik

9.1 Branch va o'lchov

Bu va keyingi dars — kanonda bitta qadam (holat 02). Bu darsda serverga tegishli qismni tayyorlaymiz, router — keyingi darsda, commit ham o'sha yerda.

bash
git switch -c feat/sahifalar

Muammo «O'lchov: v8 + 404.html» bo'limida ko'rindi: Pages'da chuqur havola ochilmaydi (zaxira yo'q), ochilsa ham fayllar topilmaydi (nisbiy yo'l). Uchta tuzatish kerak.

9.2 Absolyut yo'llar: base — BASE_PATH dan

vite.config.ts da (kanondan, aynan):

ts
    // 19/#02: yo'llar ABSOLYUT ("/vazifalar/assets/..."). 18-qismgacha
    // nisbiy edi ("./") — bitta sahifada ishlardi; /vazifalar/vazifa/2
    // dan esa "./assets/" → /vazifalar/vazifa/assets/ (404, o'lchandi:
    // natijalar/01-chuqur-havola-v8.txt). Papka nomi — BASE_PATH dan
    // (pages.yml: repo nomi), bo'lmasa "/" (Vercel, lokal sinov)
    base: env.BASE_PATH || "/",

env — loadEnv natijasi (16-qismdagi vite.config.ts). Papka nomi kodga yozilmaydi: GitHub Actions uni repo nomidan oladi. pages.yml ga:

yaml
      # 19/#02: sayt login.github.io/<repo>/ da — yo'llar absolyut
      # (vite.config.ts base), papka nomi repo nomidan
      - run: npm run build
        env:
          BASE_PATH: /${{ github.event.repository.name }}/

Vite <script> va <link rel="stylesheet"> yo'llarini o'zi almashtiradi. Lekin index.html dagi boshqa havolalarni (manifest, ikonka, tema.js) qo'lda yozganmiz. Ular uchun Vite'ning %BASE_URL% o'rinbosari bor — build'da base qiymatiga almashadi:

html
  <link rel="manifest" href="%BASE_URL%manifest.webmanifest">
  <link rel="icon" href="%BASE_URL%assets/ikonlar/ikon.svg" type="image/svg+xml">
  <link rel="apple-touch-icon" href="%BASE_URL%assets/ikonlar/ikon-180.png">
  <script src="%BASE_URL%assets/js/tema.js"></script>

JS ichida o'sha qiymat — import.meta.env.BASE_URL. Kanonda u bilan SW manzili ham absolyut bo'ldi (xavfsiz-manzil.ts): nisbiy "sw.js" /vazifalar/vazifa/2 sahifasida /vazifalar/vazifa/sw.js bo'lardi.

9.3 404.html — build'ning o'zi oladi

Vite plagini (Vite plaginlari) build tugagach index.html ni nusxalaydi. writeBundle — Vite fayllarni diskka yozib bo'lgach chaqiradigan bosqich. apply: "build" — faqat build'da, dev serverda emas:

ts
      {
        // SPA zaxirasi (19/#02): GitHub Pages /vazifalar/vazifa/2 ni
        // topolmaydi (bunday fayl yo'q) va 404.html ni beradi — u
        // index.html ning nusxasi bo'lsa, ilova ochiladi va sahifani
        // router tanlaydi (HTTP status — 404). Vercel'da kerak emas:
        // vercel.json "rewrites" index.html ni 200 bilan beradi
        name: "spa-zaxira",
        apply: "build",
        writeBundle(chiqish) {
          const papka = chiqish.dir ?? "dist";
          copyFileSync(
            resolve(papka, "index.html"),
            resolve(papka, "404.html"),
          );
        },
      },

copyFileSync va resolve — node:fs va node:path dan (16-qismda ko'rgansiz). Nusxa har build'da yangidan olinadi — hash'li nomlar doim to'g'ri. skriptlar/qobiq.js esa 404.html ni SW qobig'iga qo'shmaydi: u faqat Pages serveri uchun.

9.4 Service Worker: har sahifa — index.html

public/sw.js dagi navigatsiya tekshiruvi:

diff
-    // Faqat ilova sahifasi: /vazifalar/ yoki /vazifalar/index.html
-    const sahifami = /\/(index\.html)?$/.test(url.pathname);
+    const faylmi = /\.[a-z0-9]+$/i.test(url.pathname);
+    const sahifami = !faylmi || url.pathname.endsWith(".html");
     return sahifami ? "sahifa" : null;

Kanondagi besh qatorli izoh bu yerda qisqartirildi: ilova sahifalarga bo'lingan, hammasi bitta index.html, sahifani router tanlaydi; kengaytmali fayl — sahifa emas. Yangi node testi buni tekshiradi: tarmoq yo'q, so'rov — vazifa/2, javob — kesh: index.html. Testlar: node 173 → 174.

Hozircha ilova o'zgarmadi — router yo'q, hamma manzilda ro'yxat. Lekin endi u istalgan chuqurlikdagi manzildan ochiladi. Keyingi darsda unga sahifalarni o'rgatamiz.

10. Mashqlar

Mashqlar kurs/mashqlar/19/01-spa-routing/ da. Jonli yechimlarni «Ishga tushir» bilan sinang.

1-mashq (oson): serverga boradimi?

SPA zaxirasi sozlangan vazifalar da. Har harakat uchun yozing: serverga sahifa so'rovi ketadimi (ha yoki yo'q)?

  • «Statistika» havolasi bosildi:
  • Brauzerning «Orqaga» tugmasi bosildi:
  • Statistika sahifasida F5 bosildi:
  • Sardor Telegram'dagi havolani ochdi:
  • #/statistika dan #/sozlamalar ga o'tildi:
Yechim

Ilova ichidagi o'tishlar — pushState, popstate, hashchange — so'rovsiz. Sahifa yangidan yuklangan har holatda (F5, tashqi havola, manzil satri) so'rov serverga boradi. Aynan shu so'rovlar uchun SPA zaxirasi kerak.

2-mashq (o'rta): «Oldinga» tugmasi

«Uch vazifa — bitta komponentda» bo'limidagi routerga «Oldinga» tugmasini qo'shing. U index ni bitta oldinga siljitsin, lekin entries oxiridan o'tib ketmasin. Sinov: «Menyu», «Aloqa», «Orqaga», «Orqaga», «Oldinga» — ekranda menyu bo'lishi kerak.

Ishora: goBack dagi Math.max(0, …) ning teskarisi — Math.min(…, m.entries.length - 1).

Yechim
jsx
import { useState } from "react";

const routes = {
  "/": () => <h1>Bahor oshxonasi</h1>,
  "/menyu": () => <h1>Menyu: osh, lag'mon, manti</h1>,
  "/aloqa": () => <h1>Tel: +998 90 000 00 00</h1>,
};

function Link({ to, onNavigate, children }) {
  function handleClick(event) {
    if (event.ctrlKey || event.metaKey || event.shiftKey) return;
    event.preventDefault();
    onNavigate(to);
  }
  return (
    <a href={to} onClick={handleClick}>
      {children}
    </a>
  );
}

function App() {
  const [memory, setMemory] = useState({
    entries: ["/"],
    index: 0,
  });
  const path = memory.entries[memory.index];

  function navigate(to) {
    setMemory((m) => ({
      entries: [...m.entries.slice(0, m.index + 1), to],
      index: m.index + 1,
    }));
  }

  function go(delta) {
    // -1 — orqaga, +1 — oldinga; chegaradan chiqmaydi
    setMemory((m) => ({
      ...m,
      index: Math.min(
        Math.max(0, m.index + delta),
        m.entries.length - 1,
      ),
    }));
  }

  console.log("manzil:", path, "| index:", memory.index);
  const Page = routes[path];
  return (
    <div>
      <nav>
        <Link to="/menyu" onNavigate={navigate}>Menyu</Link>
        {" | "}
        <Link to="/aloqa" onNavigate={navigate}>Aloqa</Link>
      </nav>
      <button onClick={() => go(-1)}>Orqaga</button>
      <button onClick={() => go(1)}>Oldinga</button>
      <Page />
    </div>
  );
}

Konsolda:

text
manzil: / | index: 0
manzil: /menyu | index: 1
manzil: /aloqa | index: 2
manzil: /menyu | index: 1
manzil: / | index: 0
manzil: /menyu | index: 1

Ekranda:

text
Menyu | Aloqa
OrqagaOldinga
Menyu: osh, lag'mon, manti

Ikki tugma bitta go(delta) funksiyasiga yig'ildi — brauzerdagi history.go(-1) va history.go(1) ham shunday. Math.min(Math.max(…)) — qiymatni 0 … length - 1 oralig'ida ushlab turadi. react-router dagi createMemoryHistory da xuddi shu ish clampIndex funksiyasi bilan qilinadi.

3-mashq (qiyin): qaysi bosishni router ushlaydi?

React Router'ning Link i bosishni ushlashdan oldin tekshiradi. shouldHandleClick(click) funksiyasini yozing. U true qaytarsin, faqat agar:

  • chap tugma bosilgan (button === 0);
  • Ctrl, Cmd (metaKey), Shift, Alt bosilmagan;
  • havolada target="_blank" yo'q;
  • havola o'z sayti ichida (origin bir xil).

click — oddiy obyekt: { button, ctrlKey, metaKey, shiftKey, altKey, target, href }. Sayt manzili — https://example.com. Havolaning origin'ini new URL(href, site).origin beradi (URL va URLSearchParams).

js
const site = "https://example.com";
const plain = { button: 0, ctrlKey: false, metaKey: false,
  shiftKey: false, altKey: false, target: "", href: "/menyu" };
Yechim
js
const site = "https://example.com";

function shouldHandleClick(click) {
  const modified =
    click.ctrlKey || click.metaKey || click.shiftKey || click.altKey;
  if (click.button !== 0 || modified) return false; // brauzerga
  if (click.target === "_blank") return false; // yangi tab so'ralgan
  return new URL(click.href, site).origin === new URL(site).origin;
}

const plain = { button: 0, ctrlKey: false, metaKey: false,
  shiftKey: false, altKey: false, target: "", href: "/menyu" };

const withCtrl = { ...plain, ctrlKey: true };
const middle = { ...plain, button: 1 };
const newTab = { ...plain, target: "_blank" };
const external = { ...plain, href: "https://example.org/" };

console.log(shouldHandleClick(plain)); // true
console.log(shouldHandleClick(withCtrl)); // false
console.log(shouldHandleClick(middle)); // false
console.log(shouldHandleClick(newTab)); // false
console.log(shouldHandleClick(external)); // false

button: 1 — sichqonchaning o'rta tugmasi (yangi tab). Tashqi saytga havolada pushState umuman ishlamasdi — boshqa origin'ga yozib bo'lmaydi. Shuning uchun uni ham brauzerga qoldiramiz: oddiy o'tish. React Router (8.4.0) ichidagi shouldProcessLinkClick funksiyasi ham shu shartlarni tekshiradi (tugma, modifikatorlar, target). Tashqi havolani esa Link alohida aniqlaydi — absolyut manzil bo'lsa, oddiy <a> kabi qoldiradi.

4-mashq: vazifalar ga nazar

feat/sahifalar branch'ida «Vazifalar qadami» dagi to'rt o'zgarishni kiriting: base, pages.yml, %BASE_URL%, spa-zaxira plagini va sw.js. Keyin Git Bash'da build qiling:

bash
BASE_PATH=/vazifalar/ npm run build

dist/ da 404.html borligini va u index.html bilan bir xil ekanini tekshiring. dist/index.html ni oching: tema.js qaysi yo'l bilan yozilgan?

Yechim

dist/404.html bor, mazmuni dist/index.html bilan bir xil (diff dist/index.html dist/404.html — bo'sh chiqish). Yo'llar absolyut: <script src="/vazifalar/assets/js/tema.js">, manifest — /vazifalar/manifest.webmanifest. BASE_PATH siz build qilsangiz — /assets/js/tema.js: Vercel va lokal sinov uchun. npm test — node testlari 174 ta (yangisi — SW chuqur havola).

11. Real ishda

  • Deploy savoli №1: "Sahifani yangilasam 404" — SPA'ni birinchi marta joylashtirgan dasturchining eng ko'p savoli. Javob har hostingda boshqacha, lekin g'oya bitta: zaxira.
  • Monorepo va papkadagi ilova: kompaniya saytida ilova ko'pincha /admin/ yoki /app/ papkasida. Vite base va router'ning basename i aynan shu uchun — keyingi darsda.
  • SEO va birinchi ochilish: SPA'da birinchi HTML deyarli bo'sh. Google JS'ni bajaradi, lekin kechroq va hammasini emas. Ochiq kontentli saytlar (do'kon, blog) shuning uchun sahifani serverda tayyorlaydi — Next.js qismida.
  • Intervyu savollari: "SPA va MPA farqi?", "SPA'da F5 bosilsa nima bo'ladi?", "Hash va History routing farqi?", "Router <a> ni nega ishlatadi?".

Xulosa

  • Client routing: havola → preventDefault + pushState + qayta chizish; «Orqaga» → popstate. Server bexabar.
  • Router uch ish qiladi: manzildan sahifa tanlaydi, havolani ushlaydi, tarixni kuzatadi. Manzil — state, sahifa — uning funksiyasi.
  • F5 va tashqi havola — server so'rovi. Server har noma'lum yo'lga index.html bersin (SPA zaxirasi); Pages'da — 404.html nusxasi, holat 404.
  • Nisbiy yo'llar chuqur havolada buziladi (v8: 4 fayl 404) — base absolyut, %BASE_URL%.
  • Hash routing zaxirasiz ishlaydi, lekin # ni egallaydi va mavjud havolalarni o'zgartiradi — kanon History API'ni tanladi.

Keyingi dars: React Router I: sozlash, Route va Link — tayyor router: o'rnatish, marshrutlar, Link, NavLink va vazifalar ning birinchi uch sahifasi.

Manbalar

  • HTML Living Standard: "Session history", "The History interface" — html.spec.whatwg.org
  • MDN: "Working with the History API", "Window: popstate event" — developer.mozilla.org
  • Vite: "Deploying a Static Site" (GitHub Pages, base), "Env Variables and Modes" (BASE_URL) — vite.dev
  • Vercel: "Project Configuration — rewrites" — vercel.com/docs
  • GitHub Pages: "Creating a custom 404 page" — docs.github.com
  • react-router 8.4.0 manba kodi (createMemoryHistory, getUrlBasedHistory, shouldProcessLinkClick) — npm paketi
Ulashish:Telegram'da

Izohlar (0)

Izoh yozish uchun kiring.

  • Hozircha izoh yo'q. Birinchi bo'ling!
SPA routing qanday ishlaydi: History API, server zaxirasi va hash routing — IlmHamroh