Aplikacioni juaj është gati. Ju keni një backend që bën disa gjëra magjike dhe pastaj ekspozon disa të dhëna përmes një API. Ju keni një frontend që konsumon atë API dhe i tregon të dhënat përdoruesit. Ju jeni duke përdorur API Fetch për të bërë kërkesa në backend tuaj, pastaj përpunoni përgjigjen dhe përditësoni UI. I thjeshtë dhe i thjeshtë, apo jo? E pra, në zhvillim, po. Pastaj, ju vendosni aplikacionin tuaj në prodhim. Dhe gjëra të çuditshme fillojnë të ndodhin. Shumicën e kohës, gjithçka duket në rregull, por nganjëherë, kërkesat dështojnë. UI thyen. Përdoruesit ankohen. Ju pyesni se çfarë shkoi keq. Rrjeti është i paparashikueshëm, dhe ju duhet të jeni gati për të. Ju do të keni më mirë përgjigje për këto pyetje: Çfarë ndodh kur rrjeti është i ngadalshëm ose jo i besueshëm? Çfarë ndodh kur backend është poshtë ose kthen një gabim? Nëse konsumoni API të jashtme, çfarë ndodh kur goditni limitin e normës dhe bllokoheni? Sinqerisht, vanilla Fetch nuk është e mjaftueshme për të trajtuar këto skenarë.Ju duhet të shtoni një shumë të kodit boilerplate për të trajtuar gabimet, retries, timeouts, caching, etj Kjo mund të bëhet shpejt rrëmujë dhe e vështirë për të mirëmbajtur. Në këtë artikull, ne do të eksplorojmë se si të bëni kërkesat tuaja të marrjes të gatshme për prodhim duke përdorur një bibliotekë të quajtur Ne do të: ffetch Ndërtoni një backend me Node.js dhe Express me disa pika përfundimtare ndërtoni një frontend që sondazon ato pika të fundit me vanilla JavaScript duke përdorur API Fetch Bëni backend flaky për të simuluar skenarët e botës reale shihni se si kërkesa e marrjes mund të dështojë dhe si të merren me këto dështime futni ffetch për të thjeshtuar dhe përmirësuar trajtimin e kërkesave fetch BoilerPlatë Ne do të ndërtojmë bazat e një liste të thjeshtë të detyrave për shumë përdorues. Backend do të ekspozojë Endpoints RESTful për të krijuar, lexuar, përditësuar dhe fshirë përdoruesit dhe detyrat, dhe do të caktojë detyrat për përdoruesit. mbrapa Ne do të përdorim Node.js dhe Express për të ndërtuar backend. Ne gjithashtu do të përdorim një magazinë të thjeshtë të të dhënave në kujtesë për të mbajtur gjërat të thjeshta. interface User { id: number; // Unique identifier name: string; // Full name email: string; // Email address } export interface Task { id: number; // Unique identifier title: string; // Short task title description?: string; // Optional detailed description priority: "low" | "medium" | "high"; // Task priority } Ne do të krijojmë pikat e mëposhtme: GET /users: Merrni të gjithë përdoruesit (rrethon një grup idesh përdorues) POST /users: Krijo një përdorues të ri (kthyer ID-në e përdoruesit të krijuar) GET /users/:id: Merrni një përdorues nga id (mbërthen objektin e përdoruesit) PUT /users/:id: Përditësoni një përdorues (mbërthen një mesazh të suksesit) Delete /users/:id: fshij një përdorues (kthyer një mesazh të suksesit) GET /tasks: Merrni të gjitha detyrat (mbërthen një grup idesh të detyrave) GET /tasks/:id: Merrni një detyrë me id (kthyer objektin e detyrës) POST /tasks: Krijimi i një detyre të re (rrotullon id-në e detyrës së krijuar) PUT /tasks/:id: Përditësoni një detyrë (kthyer një mesazh të suksesit) Delete /tasks/:id: fshij një detyrë GET /users/:userId/tasks: Merrni të gjitha detyrat e caktuara për një përdorues (mbërthen një grup idesh të detyrave) POST /users/:userId/tasks/:taskId: Të caktojë një detyrë për një përdorues (kthyer një mesazh të suksesit) DELETE /users/:userId/tasks/:taskId: Heq një detyrë nga një përdorues (kthyer një mesazh të suksesit) frontit të Meqenëse kornizat zakonisht shtojnë abstraksionet dhe mënyrat e tyre për të bërë gjërat, ne do të përdorim Vanilla TypeScript për të mbajtur gjërat të thjeshta dhe jo-kornizë. Ne do të krijojmë një SPA me dy pamje: një për listën e përdoruesve dhe një për një përdorues të veçantë. Lista e përdoruesve tregon emrin e përdoruesit dhe numrin e detyrave të caktuara për ta. Klikimi në një përdorues do t'ju çojë në pamjen e përdoruesit, e cila tregon detajet e përdoruesit dhe detyrat e tyre. Për të mbajtur gjërat të thjeshta, ne do të përdorim sondazhet për të marrë të dhënat më të fundit nga backend. Çdo 3 sekonda, ne do të bëjmë kërkesa në backend për të marrë të dhënat më të fundit për pamjen aktuale dhe për të përditësuar UI përkatësisht. Për shikimin e listës së përdoruesve, ne do të bëjmë një kërkesë për për të marrë të gjitha ID-të e përdoruesit, atëherë për çdo përdorues, ne do të bëjmë një kërkesë për për të gjetur detajet e tyre, dhe për të llogaritur numrin e detyrave të caktuara për ta. GET /users GET /users/:id GET /users/:id/tasks Për shikimin e përdoruesit, do të bëjmë një kërkesë për për të marrë detajet e përdoruesit, dhe Për secilën detyrë, ne do të bëjmë një kërkesë për Për të gjetur detajet e detyrës. GET /users/:id GET /users/:id/tasks GET /tasks/:id Përshkrimi GitHub Repo Ju mund të gjeni kodin e plotë për këtë shembull në . Përshkrimi i GitHub Repo Për shkak të sasisë së boilerplate, referojuni repo për kodin e plotë. Çdo fazë e artikullit do të referohet një degë në repo. Repo përmban të dy kodin backend dhe frontend. Fjalë kyçe Frontend është në Kur të klononi repo, drejto në të dy dosjet për të instaluar varësitë. Pastaj ju mund të drejtuar backend me Në të Fjalë kyçe dhe frontin me Në të Fjalë kyçe Frontend do të shërbejë në dhe mbrapa në . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Pasi të keni bërë të gjitha detyrat dhe të dy backend tuaj dhe frontend janë duke u drejtuar, ju mund të hapni shfletuesin tuaj dhe të shkoni në Për të parë aplikacionin në veprim: http://localhost:5173 në zhvillim Nëse jeni duke lundruar , ju duhet të shihni se çdo gjë punon vetëm mirë. Nëse shtoni një përdorues të ri me http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' ju duhet të shihni përdoruesi shfaqet në shikimin e listës së përdoruesve brenda 3 sekondave. ndjehuni të lirë për të luajtur rreth me aplikacionin dhe shtoni më shumë përdorues dhe detyra. E pra, kjo është ajo ku ne më në fund arrijmë në pikën e këtij artikulli. Backend ynë punon vetëm mirë. frontend tonë, pavarësisht boilerplate tmerrshme, gjithashtu punon vetëm mirë. Por midis frontend dhe backend, ka rrjetin. Dhe rrjeti është i pasigurt. Pra, le të shohim se çfarë ndodh nëse ne shtojmë pak flakiness në backend tonë. Simulimi i gabimeve të rrjetit Le të shtojmë një middleware në backend tonë që në mënyrë të rastësishme dështon kërkesat me një shans 20% dhe gjithashtu shton disa vonesa të rastësishme deri në 1 sekondë. Ju mund të gjeni mesataren e shurdhër në Këtu është kodi: backend/src/middleware/flaky.ts import { Request, Response, NextFunction } from 'express'; export function flaky(req: Request, res: Response, next: NextFunction) { // Randomly fail requests with a 20% chance if (Math.random() < 0.2) { return res.status(500).json({ error: 'Random failure' }); } // Add random delay up to 2 seconds const delay = Math.random() * 2000; setTimeout(next, delay); } Pastaj, ne mund ta përdorim këtë middleware në aplikacionin tonë Express. Vetëm të importoni middleware dhe të përdorni atë para rrugëve tuaja: backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware Ky kod është në degë e repo, kështu që ju mund të kontrolloni atë me . network-errors git checkout network-errors Tani, nëse rifilloni backend tuaj dhe rifreskoni frontend, duhet të filloni të shihni disa gjëra të çuditshme. Dhe kjo është kur, nëse nuk e keni bërë tashmë, duhet të filloni të mendoni se si t’i menaxhoni këto gabime me hir. undefined Skenari i gabuar Së pari, le të identifikojmë se çfarë mund të shkojë keq dhe si mund ta trajtojmë atë: Dështimet e rrjetit të ndërprerë: Kërkesat mund të dështojnë rastësisht, kështu që në disa gabime, ne duhet t'i rishikojmë disa herë para se të heqim dorë. Kur sondazhojmë, ne nuk dërgojmë vetëm një kërkesë, por kërkesa të shumta në mënyrë asimetrike. Dhe 3 sekonda më vonë, ne dërgojmë një sërë tjetër kërkesa. Nëse një kërkesë nga bateria e mëparshme është ende në pritje kur dërgohet bateria e ardhshme, ne mund të marrim një përgjigje më të hershme pas një të mëvonshme. Kjo mund të çojë në një gjendje të paqëndrueshme të UI. Ne duhet të sigurohemi që vetëm përgjigja e fundit përdoret për të përditësuar UI, kështu që kur fillon një cikël i ri i sondazhit, ne duhet të anulojmë çdo kërkesë në pritje nga cikli i mëparshëm. Në mënyrë të ngjashme, nëse përdoruesi shkon në një pamje të ndryshme ndërsa kërkesa nga pamja e mëparshme janë ende në pritje, ne mund të marrim përgjigje për pamjen e mëparshme pasi kemi shkuar tashmë larg.Kjo gjithashtu mund të çojë në një gjendje të paqëndrueshme të UI. Ne duhet të sigurohemi që vetëm përgjigjet për pamjen aktuale përdoren për të përditësuar UI, kështu që kur shkonim në një pamje të ndryshme, ne duhet të anulojmë çdo kërkesa në pritje nga pamja e mëparshme. Nëse një kërkesë ishte e suksesshme në një pikë, por pastaj dështon në një cikël të mëvonshëm të sondazhit, ne nuk duam të tregojmë menjëherë një gjendje gabimi për përdoruesin. Ne duhet të trajtojmë skenarët ku, për shembull, po shohim një përdorues që është fshirë në backend.Ne duhet të trajtojmë gabimet 404 me hir dhe të lundrojmë përsëri në pamjen e listës së përdoruesve, ose të paktën të tregojmë një mesazh që nuk u gjet. Gjithashtu, ne duhet të trajtojmë skenarë ku backend është plotësisht poshtë ose i papërshtatshëm.Ne duhet të tregojmë një mesazh gabimi global për përdoruesin dhe ndoshta të rishikojmë kërkesat pas një kohe. Dhe lista vazhdon, veçanërisht nëse UI lejon krijimin, përditësimin ose fshirjen e të dhënave. por për momentin, le të përqëndrohemi në operacionet e leximit dhe se si të trajtojmë gabimet kur marrim të dhëna. Përdorimi i Vanilla Fetch Këtu, si me shumë gjëra në JavaScript (ose TypeScript), ju keni dy opsione për të trajtuar këto skenarë.Ju mund të shkruani funksionet tuaja të shërbimit për të mbështjellë API Fetch dhe të shtoni logjikën e nevojshme të trajtimit të gabimeve, ose ju mund të zgjidhni një bibliotekë që e bën këtë për ju. Le të fillojmë me implementimin e gjithçkaje vetë.Kodi është në degë e repo, kështu që ju mund të kontrolloni atë me . native-fetch git checkout native-fetch Çfarë duhet bërë Centralizoni të gjitha logjikat fetch në poller.ts. Për çdo sondazh, krijoni një AbortController të ri dhe anuloni atë të mëparshëm. Wrap merr thirrjet në një retry-and-timeout funksion. Në sukses, përditësoni një cache dhe përdoreni atë për renderim. Në dështim, rishikoni sipas nevojës, dhe të trajtojë timeouts / anulimet me hir. jonë Dosja tani duket kështu: poller.ts // Cache for responses const cache: Record<string, any> = {}; // AbortController for cancelling requests let currentController: AbortController | undefined; // Helper: fetch with retries and timeout async function fetchWithRetry(url: string, options: RequestInit = {}, retries = 2, timeout = 3000): Promise<any> { for (let attempt = 0; attempt <= retries; attempt++) { const controller = new AbortController(); const timer = setTimeout(() => controller.abort(), timeout); try { const res = await fetch(url, { ...options, signal: controller.signal }); clearTimeout(timer); if (!res.ok) throw new Error(`HTTP ${res.status}`); const data = await res.json(); return data; } catch (err) { clearTimeout(timer); if (attempt === retries) throw err; } } } // Cancel all previous requests function cancelRequests() { if (currentController) currentController.abort(); currentController = new AbortController(); } export async function fetchUserListData() { cancelRequests(); // Use cache if available if (cache.userList) return cache.userList; try { if (!currentController) throw new Error('AbortController not initialized'); const userIds = await fetchWithRetry('http://localhost:3000/users', { signal: currentController!.signal }); const users = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}`, { signal: currentController!.signal }))); const taskCounts = await Promise.all(userIds.map((id: number) => fetchWithRetry(`http://localhost:3000/users/${id}/tasks`, { signal: currentController!.signal }).then((tasks: any[]) => tasks.length))); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { // fallback to cache if available if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { if (!currentController) throw new Error('AbortController not initialized'); const user = await fetchWithRetry(`http://localhost:3000/users/${userId}`, { signal: currentController!.signal }); const taskIds = await fetchWithRetry(`http://localhost:3000/users/${userId}/tasks`, { signal: currentController!.signal }); const tasks = await Promise.all(taskIds.map((id: number) => fetchWithRetry(`http://localhost:3000/tasks/${id}`, { signal: currentController!.signal }))); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } Ne fshijmë Si çdo logjikë fetch tani është në Në rastin tonë të thjeshtuar të përdorimit, ajo është e durueshme, por edhe këtu, ne kemi nevojë për të shtuar një shumë të kodit boilerplate për të trajtuar gabimet, retries, timeouts, anulimet, dhe caching. api.ts poller.ts Nëse ju drejtuar aplikacionin tani, ju duhet të shihni se ajo punon shumë më mirë. UI është më e qëndrueshme dhe nuk thyen aq shpesh. Ju ende mund të shihni disa gabime në console, por ato janë trajtuar me hir dhe nuk ndikojnë në përvojën e përdoruesit aq shumë. Dobësitë e kësaj qasjeje Më shumë kod boilerplate: Ne kemi pasur për të shkruar një shumë të kodit për të trajtuar gabimet, retries, timeouts, anulimet, dhe caching. Jo shumë e ripërdorshme: Kodi është i lidhur ngushtë me rastin tonë specifik të përdorimit dhe jo shumë i ripërdorshëm për projekte ose skenarë të tjerë. Karakteristikat e kufizuara: Kodi merret vetëm me skenarët e gabimit bazë.Skenarët më kompleksë të tilla si backkoff eksponenciale, ndërprerësit e qarqeve ose trajtimi global i gabimeve do të kërkonin edhe më shumë kod. Përdorimi Për një trajtim më të mirë Fletë Fletë Për të adresuar disavantazhet e trajtimit tonë të personalizuar të marrjes, kam shkruar një bibliotekë të quajtur Kjo është një bibliotekë e vogël dhe e lehtë që mbështjell API Fetch dhe ofron një mënyrë të thjeshtë dhe deklarative për të trajtuar gabimet, ripries, timeouts, anulimet dhe disa veçori të tjera. ffetch Le të rishkruajmë logjikën tonë fetch duke përdorur Ju mund të gjeni kodin në degë e repo, kështu që ju mund të kontrolloni atë me . ffetch ffetch git checkout ffetch Së pari, instaloni Në të Fjalë kyçe: ffetch frontend npm install @gkoos/ffetch Ne mund të rishkruajmë Faqe që përdorin : poller.ts ffetch import createClient from '@gkoos/ffetch'; // Cache for responses const cache: Record<string, any> = {}; // Create ffetch client const api = createClient({ timeout: 3000, retries: 2, }); function cancelRequests() { api.abortAll(); } export async function fetchUserListData() { cancelRequests(); if (cache.userList) return cache.userList; try { const userIds = await api('http://localhost:3000/users').then(r => r.json()); const users = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}`).then(r => r.json())) ); const taskCounts = await Promise.all( userIds.map((id: number) => api(`http://localhost:3000/users/${id}/tasks`).then(r => r.json()).then((tasks: any[]) => tasks.length)) ); cache.userList = { users, taskCounts }; return cache.userList; } catch (err) { if (cache.userList) return cache.userList; throw err; } } export async function fetchUserDetailsData(userId: number) { cancelRequests(); const cacheKey = `userDetails_${userId}`; if (cache[cacheKey]) return cache[cacheKey]; try { const user = await api(`http://localhost:3000/users/${userId}`).then(r => r.json()); const taskIds = await api(`http://localhost:3000/users/${userId}/tasks`).then(r => r.json()); const tasks = await Promise.all( taskIds.map((id: number) => api(`http://localhost:3000/tasks/${id}`).then(r => r.json())) ); cache[cacheKey] = { user, tasks }; return cache[cacheKey]; } catch (err) { if (cache[cacheKey]) return cache[cacheKey]; throw err; } } Kodi është shumë më i pastër dhe më i lehtë për t'u lexuar.Ne nuk duhet të shqetësohemi më për riparime, afate ose anulime. Ne thjesht krijojmë një klient me opsionet e dëshiruara dhe e përdorim atë për të bërë kërkesa. ffetch Përfitime të tjera nga përdorimi ffetch Rrotullimi i qarkullimit: ngrohje automatike e pikës së fundit pas dështimeve të përsëritura Mbështetje automatike eksponenciale për rikthimet: rritja e kohës së pritjes midis rikthimeve Menaxhimi global i gabimeve: hooks për logging, modifikimin e kërkesave / përgjigjeve, etj. Për shembull, ne mund të zgjedhim të rishikojmë në gabimet e rrjetit dhe gabimet e serverit 5xx, por jo në gabimet e klientit 4xx. nuk bën asgjë magjike që nuk mund të ndërtoni vetë, por ju shpëton nga shkrimi, testimi dhe mirëmbajtja e të gjitha këtyre boilerplate. Është një mbështjellës komoditeti që bakes në modelet e shkallës së prodhimit (të tilla si shkatërruesi i qarkullimit dhe backoff) në mënyrë që të mund të përqendroheni në aplikacionin tuaj, jo logjikën tuaj të marrjes. Ajo gjithashtu ndalet në shtresën e marrjes, kështu që ju ende mund të përdorni caching tuaj, menaxhimin e shtetit dhe bibliotekat e UI siç e shihni të përshtatshme. ffetch Konkludimi Pika kryesore e këtij artikulli nuk është se ju duhet të përdorni veçanërisht, por që ju nuk duhet të mbështeteni në vanilla Fetch për aplikacionet e gatshme të prodhimit. Rrjeti është i pasigurt, dhe ju duhet të jeni të përgatitur për të. ffetch Çfarë saktësisht duhet të bëni varet nga rasti dhe kërkesat tuaja specifike të përdorimit, por ju nuk mund të shkoni në prodhim që merret vetëm me rrugën e lumtur. gjërat mund dhe do të shkojnë keq, dhe aplikacioni juaj duhet të trajtojë të paktën skenarët më të zakonshëm të dështimit. Mund të ndihmojë me këtë. ffetch