20-bo‘lim
Amaliy loyiha - kutubxona mijozi
Node.js darsligida yozilgan API ga ulanadigan to'liq ilova: marshrutlash, ma'lumot olish, forma, kontekst va testlar.
Ushbu bo‘lim mundarijasi
Node.js darsligining 20-bo'limida kutubxona API sini yozgan edik. Endi unga ulanadigan React ilovasini quramiz.
Bu bo'limda yigirma bo'limda o'rgangan hamma narsa birlashadi.
Loyiha tuzilishi #
API qatlami #
export function apiYarat(asos) {
async function sora(yol, sozlama = {}) {
const javob = await fetch(asos + yol, {
headers: { "Content-Type": "application/json" },
...sozlama,
});
const matn = await javob.text();
const tana = matn ? JSON.parse(matn) : null;
if (!javob.ok) {
const xabar = tana?.xato?.xabar ?? `holat ${javob.status}`;
const xato = new Error(xabar);
xato.holat = javob.status;
xato.kod = tana?.xato?.kod ?? "NOMALUM";
throw xato;
}
return tana;
}
return {
royxat: (sahifa = 1) => sora(`/kitoblar?sahifa=${sahifa}&hajm=10`),
bittasi: (id) => sora(`/kitoblar/${id}`),
qosh: (kitob) => sora("/kitoblar", {
method: "POST",
body: JSON.stringify(kitob),
}),
ochir: (id) => sora(`/kitoblar/${id}`, { method: "DELETE" }),
};
}
Tarmoq bilan ishlash butunlay shu faylda. Komponentlar
fetch ni ko'rmaydi.
javob.ok tekshiruvi (11-bo'lim) va serverning xato shakli
(Node.js 15-bo'lim) shu yerda bir joyga yig'ilgan.
Kontekst va ilgak #
import { createContext, useContext, useEffect, useState } from "react";
const ApiKonteksti = createContext(null);
export function ApiTaminlovchi({ api, children }) {
return <ApiKonteksti value={api}>{children}</ApiKonteksti>;
}
export function useApi() {
const api = useContext(ApiKonteksti);
if (api === null) throw new Error("useApi ApiTaminlovchi ichida bo'lsin");
return api;
}
export function useKitoblar(sahifa = 1) {
const api = useApi();
const [holat, ozgartir] = useState({
malumot: null,
yuklanmoqda: true,
xato: null,
});
const [yangilash, yangila] = useState(0);
useEffect(() => {
let bekor = false;
ozgartir({ malumot: null, yuklanmoqda: true, xato: null });
api.royxat(sahifa)
.then((natija) => {
if (!bekor) {
ozgartir({
malumot: natija,
yuklanmoqda: false,
xato: null,
});
}
})
.catch((x) => {
if (!bekor) {
ozgartir({ malumot: null, yuklanmoqda: false,
xato: x.message });
}
});
return () => { bekor = true; };
}, [api, sahifa, yangilash]);
return { ...holat, yangila: () => yangila((n) => n + 1) };
}
useKitoblar - 15-bo'limdagi naqsh: ma'lumot olish mantig'i
bitta ilgakda.
yangilash hisoblagichi ro'yxatni qayta yuklash imkonini
beradi - kitob qo'shilgandan keyin kerak bo'ladi.
Komponentlar #
import { useState, useEffect } from "react";
import { Link, useParams, useNavigate } from "react-router";
import { useApi, useKitoblar } from "./loyiha-kontekst.mjs";
export function KitoblarSahifasi() {
const { malumot, yuklanmoqda, xato } = useKitoblar(1);
if (yuklanmoqda) return <p id="holat">Yuklanmoqda...</p>;
if (xato) return <p id="holat">Xato: {xato}</p>;
const kitoblar = malumot.malumot;
if (kitoblar.length === 0) {
return <p id="holat">Hozircha kitob yo'q</p>;
}
return (
<div>
<p id="holat">{malumot.sahifalash.jami} ta kitob</p>
<ul>
{kitoblar.map((k) => (
<li key={k.id}>
<Link to={`/kitoblar/${k.id}`}>{k.sarlavha}</Link>
{" - "}{k.muallif}
</li>
))}
</ul>
</div>
);
}
export function KitobSahifasi() {
const { id } = useParams();
const api = useApi();
const [holat, ozgartir] = useState({ kitob: null, xato: null });
useEffect(() => {
let bekor = false;
api.bittasi(Number(id))
.then((n) => {
if (!bekor) ozgartir({ kitob: n.malumot, xato: null });
})
.catch((x) => {
if (!bekor) ozgartir({ kitob: null, xato: x.message });
});
return () => { bekor = true; };
}, [api, id]);
if (holat.xato) return <p id="holat">Xato: {holat.xato}</p>;
if (!holat.kitob) return <p id="holat">Yuklanmoqda...</p>;
return (
<div>
<h1 id="holat">{holat.kitob.sarlavha}</h1>
<p>{holat.kitob.muallif}, {holat.kitob.yil}</p>
<Link to="/">Orqaga</Link>
</div>
);
}
export function YangiKitob() {
const api = useApi();
const otish = useNavigate();
const [malumot, ozgartir] = useState({
sarlavha: "", muallif: "", yil: "",
});
const [xatolar, xatolarniOzgartir] = useState({});
const [yuborilmoqda, yuborishniOzgartir] = useState(false);
function maydon(hodisa) {
const { name, value } = hodisa.target;
ozgartir((oldingi) => ({ ...oldingi, [name]: value }));
}
function tekshir() {
const topilgan = {};
if (malumot.sarlavha.trim().length < 1) {
topilgan.sarlavha = "sarlavha kerak";
}
if (malumot.muallif.trim().length < 2) {
topilgan.muallif = "muallif kamida 2 belgi";
}
const yil = Number(malumot.yil);
if (!Number.isInteger(yil) || yil < 1450) {
topilgan.yil = "yil noto'g'ri";
}
return topilgan;
}
async function yubor(hodisa) {
hodisa.preventDefault();
const topilgan = tekshir();
xatolarniOzgartir(topilgan);
if (Object.keys(topilgan).length > 0) return;
yuborishniOzgartir(true);
try {
await api.qosh({
sarlavha: malumot.sarlavha.trim(),
muallif: malumot.muallif.trim(),
yil: Number(malumot.yil),
});
otish("/");
} catch (x) {
xatolarniOzgartir({ umumiy: x.message });
} finally {
yuborishniOzgartir(false);
}
}
return (
<form onSubmit={yubor}>
<input
name="sarlavha"
value={malumot.sarlavha}
onChange={maydon}
/>
<input name="muallif" value={malumot.muallif} onChange={maydon} />
<input name="yil" value={malumot.yil} onChange={maydon} />
<button type="submit" disabled={yuborilmoqda}>
{yuborilmoqda ? "Saqlanmoqda..." : "Saqlash"}
</button>
<p id="xato">
{Object.values(xatolar).join("; ") || "xato yo'q"}
</p>
</form>
);
}
Uch sahifa, har biri o'z vazifasida.
E'tibor bering: YangiKitob da disabled={yuborilmoqda} bor -
foydalanuvchi tugmani ikki marta bosib, ikkita kitob yaratib
qo'ymasligi uchun.
Ilovani yig'ish #
import { MemoryRouter, Routes, Route, Link } from "react-router";
import { ApiTaminlovchi } from "./loyiha-kontekst.mjs";
import {
KitoblarSahifasi, KitobSahifasi, YangiKitob,
} from "./loyiha-komponentlar.mjs";
export function Ilova({ api, boshlangichYol = "/" }) {
return (
<ApiTaminlovchi api={api}>
<MemoryRouter initialEntries={[boshlangichYol]}>
<nav>
<Link to="/">Kitoblar</Link>
<Link to="/yangi">Yangi</Link>
</nav>
<Routes>
<Route path="/" element={<KitoblarSahifasi />} />
<Route path="/kitoblar/:id" element={<KitobSahifasi />} />
<Route path="/yangi" element={<YangiKitob />} />
<Route path="*" element={<p id="holat">Topilmadi</p>} />
</Routes>
</MemoryRouter>
</ApiTaminlovchi>
);
}
api prop sifatida beriladi, ichkarida yaratilmaydi.
Bu kichik qaror testlarni ancha osonlashtiradi: testda soxta
api berish yetarli.
Ishlatib ko'ramiz #
import { createServer } from "node:http";
import { apiYarat } from "./loyiha-api.mjs";
import { Ilova } from "./loyiha-ilova.mjs";
import { chiz, kut, matn } from "./dom-muhit.mjs";
const kitoblar = [
{
id: 1, sarlavha: "O'tkan kunlar",
muallif: "Abdulla Qodiriy", yil: 1926,
},
{ id: 2, sarlavha: "Sarob", muallif: "Abdulla Qahhor", yil: 1935 },
];
const server = createServer((sorov, javob) => {
const yol = sorov.url.split("?")[0];
javob.setHeader("Content-Type", "application/json");
if (yol === "/kitoblar") {
javob.writeHead(200).end(JSON.stringify({
malumot: kitoblar,
sahifalash: { sahifa: 1, hajm: 10, jami: kitoblar.length },
}));
return;
}
const mos = yol.match(/^\/kitoblar\/(\d+)$/);
const kitob = mos && kitoblar.find((k) => k.id === Number(mos[1]));
if (kitob) {
javob.writeHead(200).end(JSON.stringify({ malumot: kitob }));
return;
}
javob.writeHead(404).end(JSON.stringify({
xato: { kod: "TOPILMADI", xabar: "kitob topilmadi" },
}));
});
server.listen(0);
const api = apiYarat(`http://localhost:${server.address().port}`);
await chiz(<Ilova api={api} />);
console.log("boshida:", matn("#holat"));
await kut();
console.log("yuklangandan keyin:", matn("#holat"));
console.log("havolalar:", [...document.querySelectorAll("li a")]
.map((h) => h.textContent).join(", "));
await chiz(<Ilova key="2" api={api} boshlangichYol="/kitoblar/2" />);
await kut();
console.log("bitta kitob:", matn("#holat"));
await chiz(<Ilova key="3" api={api} boshlangichYol="/kitoblar/99" />);
await kut();
console.log("yo'q kitob:", matn("#holat"));
server.close();
boshida: Yuklanmoqda...
yuklangandan keyin: 2 ta kitob
havolalar: O'tkan kunlar, Sarob
bitta kitob: Sarob
yo'q kitob: Xato: kitob topilmadi
Serverning xato shakli mijozda tushunarli xabarga aylandi -
api.mjs dagi tarjima tufayli.
Formani sinash #
import { apiYarat } from "./loyiha-api.mjs";
import { Ilova } from "./loyiha-ilova.mjs";
import { chiz, bos, yoz, kut, matn } from "./dom-muhit.mjs";
import { createServer } from "node:http";
const qabulQilingan = [];
const server = createServer((sorov, javob) => {
javob.setHeader("Content-Type", "application/json");
if (sorov.method === "POST") {
let tana = "";
sorov.on("data", (b) => { tana += b; });
sorov.on("end", () => {
qabulQilingan.push(JSON.parse(tana));
javob.writeHead(201).end(JSON.stringify({
malumot: { id: 3, ...JSON.parse(tana) },
}));
});
return;
}
javob.writeHead(200).end(JSON.stringify({
malumot: [], sahifalash: { sahifa: 1, hajm: 10, jami: 0 },
}));
});
server.listen(0);
const api = apiYarat(`http://localhost:${server.address().port}`);
await chiz(<Ilova api={api} boshlangichYol="/yangi" />);
await bos("button");
console.log("bo'sh forma:", matn("#xato"));
await yoz("input[name=sarlavha]", "Mehrobdan chayon");
await yoz("input[name=muallif]", "A");
await yoz("input[name=yil]", "1929");
await bos("button");
console.log("qisqa muallif:", matn("#xato"));
await yoz("input[name=muallif]", "Abdulla Qodiriy");
await bos("button");
await kut();
const yuborilgan = qabulQilingan[0];
console.log("sarlavha:", yuborilgan.sarlavha);
console.log("muallif:", yuborilgan.muallif);
console.log("yil turi:", typeof yuborilgan.yil, yuborilgan.yil);
console.log("yuborilganlar soni:", qabulQilingan.length);
// Saqlangandan keyin ilova ro'yxat sahifasiga o'tdi va u ham
// so'rov yubordi - uni ham kutib olamiz.
await kut();
server.close();
bo'sh forma: sarlavha kerak; muallif kamida 2 belgi; yil noto'g'ri
qisqa muallif: muallif kamida 2 belgi
sarlavha: Mehrobdan chayon
muallif: Abdulla Qodiriy
yil turi: number 1929
yuborilganlar soni: 1
Tekshiruv ishladi va yaroqsiz ma'lumot serverga umuman yuborilmadi.
Diqqat: yil serverga son sifatida ketdi - forma uni satr
bergan edi (8-bo'lim), yubor esa Number() bilan aylantirdi.
Soxta api bilan sinash #
import { Ilova } from "./loyiha-ilova.mjs";
import { chiz, kut, matn } from "./dom-muhit.mjs";
const soxtaApi = {
royxat: async () => ({
malumot: [{ id: 1, sarlavha: "Soxta kitob", muallif: "Hech kim" }],
sahifalash: { sahifa: 1, hajm: 10, jami: 1 },
}),
bittasi: async () => { throw new Error("chaqirilmasligi kerak"); },
qosh: async () => ({}),
ochir: async () => ({}),
};
const boshApi = {
...soxtaApi,
royxat: async () => ({
malumot: [],
sahifalash: { sahifa: 1, hajm: 10, jami: 0 },
}),
};
const xatoApi = {
...soxtaApi,
royxat: async () => { throw new Error("tarmoq uzildi"); },
};
await chiz(<Ilova api={soxtaApi} />);
await kut(10);
console.log("ma'lumot bilan:", matn("#holat"));
await chiz(<Ilova key="2" api={boshApi} />);
await kut(10);
console.log("bo'sh ro'yxat:", matn("#holat"));
await chiz(<Ilova key="3" api={xatoApi} />);
await kut(10);
console.log("xato holatida:", matn("#holat"));
ma'lumot bilan: 1 ta kitob
bo'sh ro'yxat: Hozircha kitob yo'q
xato holatida: Xato: tarmoq uzildi
Server umuman ishga tushirilmadi - api oddiy obyekt bilan
almashtirildi.
Ilova ichida apiYarat(...) chaqirilganda uni sinash uchun
haqiqiy server kerak bo'lardi: sekin, mo'rt va uch holatni
(bo'sh, xato, ma'lumot) yasash qiyin.
api ni tashqaridan berish uch chiziqni ochadi:
- Har bir holatni oson yasash mumkin;
- Testlar tez ishlaydi;
- Chegaraviy holatlarni sinash arzon bo'ladi.
Bu naqsh "bog'liqlikni kiritish" deb ataladi va u React ga xos emas - hamma tilda ishlaydi.
Nimalar ishlatildi #
| Qism | Bo'lim |
|---|---|
| JSX va komponentlar | 3, 4 |
Ro'yxat va key | 5 |
| Hodisalar va holat | 6, 7 |
| Boshqariladigan forma | 8 |
useEffect va tozalash | 10 |
Ma'lumot olish, bekor bayrog'i | 11 |
| Kontekst va o'z ilgagingiz | 13, 15 |
Marshrutlash, useParams, useNavigate | 18 |
| Testlar | 19 |
Bu ilovani haqiqiy loyihaga aylantirish uchun:
TanStack Query- kesh, qayta urinish, fon yangilash (11-bo'lim);- Autentifikatsiya - Node.js darsligining 17-bo'limidagi sessiyalar bilan;
- Yuklanish ko'rinishi - bo'sh matn o'rniga skelet;
- Xato chegarasi -
ErrorBoundarybilan butun ilova qulamasin; - TypeScript - API javoblarining shaklini turlar bilan qat'iylashtirish;
- Kirish imkoniyati - klaviatura bilan yurish,
ariaatributlari.
- Loyihani fayl-fayl ko'chirib yozing.
- Node.js darsligidagi haqiqiy API ga ulang.
- O'chirish tugmasini qo'shing.
- O'chirgandan keyin ro'yxatni yangilang.
- Sahifalashni ishlatib, ikkinchi sahifaga o'ting.
- Qidiruv maydonini qo'shib, holatni manzilda saqlang.
- Yuklanish uchun skelet ko'rinish yasang.
- Soxta
apibilan uch holatni sinang. - Forma uchun React Testing Library testi yozing.
useKitoblarniTanStack Queryga almashtiring.
Xulosa #
- Tarmoq bilan ishlash bitta faylda - komponentlar
fetchni ko'rmaydi. - Serverning xato shakli mijozda tushunarli xabarga aylantiriladi.
- Ma'lumot olish mantig'i o'z ilgagingizda.
apini prop sifatida bering - testlar oson bo'ladi.- Forma tekshiruvi yaroqsiz ma'lumotni serverga yubormaydi.
- Yuborish paytida tugmani o'chiring - ikki marta yuborish oldini oladi.
- Uch holat - yuklanmoqda, bo'sh, xato - har birini alohida ko'rsating.
Tabriklaymiz - siz React ning asosiy qismini o'rgandingiz. Endi eng yaxshi davom etish yo'li - o'z loyihangizni yozish.
O‘qish tarixini saqlamoqchimisiz?
Tizimga kirsangiz, tugatgan bo‘limlaringiz saqlanadi va qoldirgan joyingizdan davom etasiz.
Xatolik topdingizmi?
Imlo xatosi, ishlamaydigan kod yoki noto‘g‘ri ma‘lumotni ko‘rsangiz - bizga xabar bering. Har bir xabar administrator tomonidan ko‘rib chiqiladi.