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.

🕑 8 daqiqa o‘qish 📄 537 so‘z 👁 0 marta ko‘rilgan
Ushbu bo‘lim mundarijasi
  1. Loyiha tuzilishi
  2. API qatlami
  3. Kontekst va ilgak
  4. Komponentlar
  5. Ilovani yig'ish
  6. Ishlatib ko'ramiz
  7. Formani sinash
  8. Soxta api bilan sinash
  9. Nimalar ishlatildi
  10. Xulosa

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 #

Har bir fayl - bitta vazifa App.jsx marshrutlar KitoblarSahifasi KitobSahifasi YangiKitob useKitoblar api.mjs tarmoq bilan ishlash faqat api.mjs da - qolgani undan foydalanadi

API qatlami #

JSX
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 #

JSX
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 #

JSX
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 #

JSX
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 #

JSX
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();
Natija
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 #

JSX
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();
Natija
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 #

JSX
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"));
Natija
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.

Bog'liqlikni prop sifatida berish

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:

  1. Har bir holatni oson yasash mumkin;
  2. Testlar tez ishlaydi;
  3. Chegaraviy holatlarni sinash arzon bo'ladi.

Bu naqsh "bog'liqlikni kiritish" deb ataladi va u React ga xos emas - hamma tilda ishlaydi.

Nimalar ishlatildi #

QismBo'lim
JSX va komponentlar3, 4
Ro'yxat va key5
Hodisalar va holat6, 7
Boshqariladigan forma8
useEffect va tozalash10
Ma'lumot olish, bekor bayrog'i11
Kontekst va o'z ilgagingiz13, 15
Marshrutlash, useParams, useNavigate18
Testlar19
Keyingi qadamlar

Bu ilovani haqiqiy loyihaga aylantirish uchun:

  1. TanStack Query - kesh, qayta urinish, fon yangilash (11-bo'lim);
  2. Autentifikatsiya - Node.js darsligining 17-bo'limidagi sessiyalar bilan;
  3. Yuklanish ko'rinishi - bo'sh matn o'rniga skelet;
  4. Xato chegarasi - ErrorBoundary bilan butun ilova qulamasin;
  5. TypeScript - API javoblarining shaklini turlar bilan qat'iylashtirish;
  6. Kirish imkoniyati - klaviatura bilan yurish, aria atributlari.
Amaliy topshiriq
  1. Loyihani fayl-fayl ko'chirib yozing.
  2. Node.js darsligidagi haqiqiy API ga ulang.
  3. O'chirish tugmasini qo'shing.
  4. O'chirgandan keyin ro'yxatni yangilang.
  5. Sahifalashni ishlatib, ikkinchi sahifaga o'ting.
  6. Qidiruv maydonini qo'shib, holatni manzilda saqlang.
  7. Yuklanish uchun skelet ko'rinish yasang.
  8. Soxta api bilan uch holatni sinang.
  9. Forma uchun React Testing Library testi yozing.
  10. useKitoblar ni TanStack Query ga almashtiring.

Xulosa #

  • Tarmoq bilan ishlash bitta faylda - komponentlar fetch ni ko'rmaydi.
  • Serverning xato shakli mijozda tushunarli xabarga aylantiriladi.
  • Ma'lumot olish mantig'i o'z ilgagingizda.
  • api ni 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.

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.