Aplicația dvs. este gata. Aveți un backend care face câteva lucruri magice și apoi expune unele date printr-un API. Aveți un frontend care consumă acel API și afișează datele utilizatorului. Folosiți API-ul Fetch pentru a face solicitări la backend-ul dvs., apoi procesați răspunsul și actualizați UI-ul. Ei bine, în dezvoltare, da. Apoi, vă implementați aplicația în producție. Și lucrurile ciudate încep să se întâmple. De cele mai multe ori, totul pare bine, dar uneori, cererile eșuează. UI-ul se rupe. Utilizatorii se plâng. Te întrebi ce a mers greșit. Rețeaua este imprevizibilă și trebuie să fiți pregătiți pentru ea. Ar fi mai bine să aveți răspunsuri la aceste întrebări: Ce se întâmplă atunci când rețeaua este lentă sau nesigură? Ce se întâmplă atunci când backend-ul este în jos sau returnează o eroare? Dacă consumați API-uri externe, ce se întâmplă atunci când atingeți limita ratei și deveniți blocați? Sincer, Vanilla Fetch nu este suficient pentru a gestiona aceste scenarii. Trebuie să adăugați o mulțime de cod boilerplate pentru a gestiona erorile, retries, timeout-uri, caching-uri etc. Acest lucru poate deveni rapid confuz și dificil de întreținut. În acest articol, vom explora cum să faceți ca solicitările dvs. de colectare să fie gata de producție folosind o bibliotecă numită Noi vom fi: ffetch construiți un backend cu Node.js și Express cu unele puncte finale construiți un frontend care sondează aceste puncte finale cu vanilie JavaScript folosind API-ul Fetch face backend-ul flaky pentru a simula scenarii din lumea reală Vezi cum pot eșua solicitările de colectare și cum pot fi gestionate aceste eșecuri Introduceți ffetch pentru a simplifica și îmbunătăți procesarea cererilor de obținere Boilerul Vom construi coloana vertebrală a unei liste simple de sarcini pentru mai mulți utilizatori. Backend-ul va expune RESTful pentru a crea, citi, actualiza și șterge utilizatorii și sarcinile și pentru a atribui sarcini utilizatorilor. Frontend-ul va sonda backend-ul pentru a obține cele mai recente date și pentru a le afișa utilizatorului. înapoi Vom folosi Node.js și Express pentru a construi backend-ul. Vom folosi, de asemenea, un depozit simplu de date în memorie pentru a menține lucrurile simple. 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 } Vom crea următoarele puncte finale: GET /users: Obțineți toți utilizatorii (returnează un set de ID-uri de utilizator) Creați un nou utilizator (returnează ID-ul utilizatorului creat) GET /users/:id: Obțineți un utilizator prin id (returnează obiectul utilizatorului) PUT /users/:id: Actualizarea unui utilizator (returnează un mesaj de succes) DELETE /users/:id: Ștergeți un utilizator (returnează un mesaj de succes) GET /tasks: Obțineți toate sarcinile (returnează un set de ID-uri de sarcină) GET /tasks/:id: Obțineți o sarcină prin id (returnează obiectul de sarcină) POST /tasks: Creați o nouă sarcină (returnează ID-ul activității create) PUT /tasks/:id: Actualizarea unei sarcini (returnează un mesaj de succes) Delete /tasks/:id: Ștergeți o sarcină GET /users/:userId/tasks: Obțineți toate sarcinile atribuite unui utilizator (returnează un set de ID-uri de sarcină) POST /users/:userId/tasks/:taskId: Atribuiți o sarcină unui utilizator (returnează un mesaj de succes) DELETE /users/:userId/tasks/:taskId: Elimină o sarcină de la un utilizator (returnează un mesaj de succes) Frontă Deoarece frameworks-urile adaugă de obicei propriile abstracții și modalități de a face lucrurile, vom folosi vanilla TypeScript pentru a păstra lucrurile simple și non-framework. Vom crea un SPA cu două vizualizări: una pentru lista de utilizatori și una pentru un anumit utilizator. Lista de utilizatori afișează numele utilizatorului și numărul de sarcini atribuite acestora. Făcând clic pe un utilizator vă va duce la vizualizarea utilizatorului, care arată detaliile utilizatorului și sarcinile lor. Pentru a păstra lucrurile simple, vom folosi sondajele pentru a obține cele mai recente date din backend. La fiecare 3 secunde, vom face solicitări către backend pentru a obține cele mai recente date pentru vizualizarea curentă și pentru a actualiza UI-ul în consecință. Pentru vizualizarea listei de utilizatori, vom face o cerere de pentru a obține toate ID-urile de utilizator, apoi pentru fiecare utilizator, vom face o cerere de pentru a-şi recâştiga detaliile şi pentru a calcula numărul de sarcini care le sunt atribuite. GET /users GET /users/:id GET /users/:id/tasks Pentru vizualizarea utilizatorului, vom face o cerere de pentru a obține detaliile utilizatorului și pentru a obține ID-urile de sarcină atribuite acestora. Apoi, pentru fiecare ID-ul de sarcină, vom face o cerere de pentru a recupera detaliile sarcinii. GET /users/:id GET /users/:id/tasks GET /tasks/:id Despre GitHub Repo Puteți găsi codul complet pentru acest exemplu în . GitHub repo Din cauza cantității de boilerplate, consultați repo pentru codul complet. Fiecare etapă a articolului va face referire la o ramură în repo. Repo-ul conține atât codul backend, cât și codul frontend. Făgăduinţa, iar frontul se află în Când clonaţi repo-ul, rulaţi ambele foldere pentru a instala dependenţele. apoi puteţi rula backend-ul cu În Folders, şi front-end cu În Front-end-ul va fi servit la În spatele lui . backend frontend npm install npm run dev backend npm run dev frontend http://localhost:5173 http://localhost:3000 Odată ce ați terminat toate sarcinile și atât backend-ul cât și frontend-ul dvs. sunt în funcțiune, puteți deschide browser-ul și mergeți la Pentru a vedea aplicația în acțiune: http://localhost:5173 în dezvoltare Dacă navigaţi pe , ar trebui să vedeți că totul funcționează bine. Dacă adăugați un nou utilizator cu http://localhost:5173 curl -X POST http://localhost:3000/users \ -H "Content-Type: application/json" \ -d '{"name": "John Doe", "email": "john@example.com"}' ar trebui să vedeți utilizatorul în vizualizarea listei de utilizatori în decurs de 3 secunde. Simțiți-vă liber să vă jucați cu aplicația și să adăugați mai mulți utilizatori și sarcini. Ei bine, aici ajungem în cele din urmă la punctul acestui articol. Backend-ul nostru funcționează doar bine. Frontend-ul nostru, în ciuda plăcii de cazan groaznice, funcționează, de asemenea, doar bine. Dar între frontend și backend, există rețeaua. Și rețeaua este nesigură. Deci, să vedem ce se întâmplă dacă adăugăm un pic de flakiness la backend-ul nostru. Simularea erorilor de rețea Să adăugăm un middleware la backend-ul nostru care eșuează aleatoriu solicitările cu o șansă de 20% și, de asemenea, adaugă o întârziere aleatorie de până la 1 secundă. Puteți găsi mijloacele flaky în Aici este codul: 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); } Apoi, putem folosi acest middleware în aplicația noastră Express. Pur și simplu importați middleware-ul și utilizați-l înainte de rutele dvs.: backend/src/index.ts ... import { flaky } from './middleware/flaky'; ... app.use(cors()); app.use(express.json()); app.use(flaky); // Use the flaky middleware Acest cod se află în ramură a repo-ului, astfel încât să o puteți verifica cu . network-errors git checkout network-errors Acum, dacă vă reporniți backend-ul și reîmprospătați front-end-ul, ar trebui să începeți să vedeți niște lucruri ciudate. Lucrurile încep să se prăbușească și acesta este momentul în care, dacă nu ați făcut-o deja, trebuie să începeți să vă gândiți cum să faceți față grațios acestor greșeli. undefined Scenarii greșite Mai întâi de toate, să identificăm ce poate merge prost și cum putem face față: Eșecuri intermitente de rețea: cererile pot eșua la întâmplare, astfel încât, în cazul anumitor erori, trebuie să le repetăm de câteva ori înainte de a renunța. În timpul sondajului, nu trimitem o singură solicitare, ci mai multe solicitări în mod asincron. Și după 3 secunde trimitem un alt lot de solicitări. Dacă o solicitare din lotul anterior este încă în așteptare atunci când este trimis următorul lot, este posibil să obținem un răspuns mai devreme după unul mai târziu. Acest lucru poate duce la o stare de UI inconsistentă. Trebuie să ne asigurăm că numai ultimul răspuns este utilizat pentru a actualiza UI-ul, astfel încât atunci când începe un nou ciclu de sondaj, trebuie să anulăm orice solicitări în așteptare din ciclul anterior. În mod similar, dacă utilizatorul navighează într-o altă vizualizare în timp ce solicitările din vizualizarea anterioară sunt încă în așteptare, este posibil să obținem răspunsuri pentru vizualizarea anterioară după ce am fost deja îndepărtați. Acest lucru poate duce, de asemenea, la o stare de UI inconsistentă. Dacă o solicitare a fost reușită la un moment dat, dar apoi eșuează într-un ciclu de sondaj ulterior, nu vrem să arătăm imediat o stare de eroare utilizatorului. Trebuie să tratăm scenarii în care, de exemplu, vedem un utilizator care a fost șters în backend.Trebuie să tratăm erorile 404 grațios și să ne întoarcem la vizualizarea listei de utilizatori sau cel puțin să afișăm un mesaj care nu a fost găsit. De asemenea, trebuie să gestionăm scenarii în care backend-ul este complet în jos sau inaccesibil.Trebuie să afișăm un mesaj de eroare global utilizatorului și, eventual, să resetăm cererile după un timp. Și lista continuă, mai ales dacă UI permite crearea, actualizarea sau ștergerea datelor. dar pentru moment, să ne concentrăm pe operațiunile de citire și cum să gestionăm erorile atunci când colectăm date. Gestionarea greșelilor cu Vanilla Fetch Aici, ca și în cazul multor lucruri în JavaScript (sau TypeScript), aveți două opțiuni pentru a gestiona aceste scenarii.Puteți scrie propriile funcții utilitare pentru a înfășura API-ul Fetch și a adăuga logica necesară de gestionare a erorilor, sau puteți alege o bibliotecă care face acest lucru pentru dvs. Să începem prin a pune totul în aplicare noi înșine.Codul este pe ramură a repo-ului, astfel încât să o puteți verifica cu . native-fetch git checkout native-fetch Ce trebuie făcut Centralizează logica fetișelor în poller.ts. Pentru fiecare sondaj, creați un nou AbortController și anulați cel anterior. Înveliți apelurile în funcție de retry-and-timeout. La succes, actualizați un cache și utilizați-l pentru rendering. La eșec, repetați după cum este necesar și gestionați timelapse / anulări grațios. a noastră Fișierul arată acum așa: 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; } } Am şters Întrucât toată logica este acum în În cazul nostru de utilizare simplificat, este ușor de suportat, dar chiar și aici, a trebuit să adăugăm o mulțime de cod boilerplate pentru a gestiona erorile, retries, timeout-uri, anulări și cache-uri. api.ts poller.ts Dacă rulați aplicația acum, ar trebui să vedeți că funcționează mult mai bine. UI-ul este mai stabil și nu se rupe la fel de des. Puteți vedea încă unele erori în consola, dar acestea sunt gestionate grațios și nu afectează atât de mult experiența utilizatorului. Dezavantajele acestei abordări Mai mult cod boilerplate: A trebuit să scriem o mulțime de cod pentru a gestiona erorile, retriile, timelapse, anulările și cache-ul. Nu este foarte reutilizabil: codul este strâns legat de cazul nostru specific de utilizare și nu este foarte reutilizabil pentru alte proiecte sau scenarii. Caracteristici limitate: Codul se ocupă numai de scenarii de eroare de bază.Scenarii mai complexe, cum ar fi retrogradarea exponențială, întrerupătorii de circuit sau gestionarea globală a erorilor ar necesita și mai mult cod. Utilizarea Pentru o mai bună manipulare ffetch fetiță Pentru a aborda dezavantajele manipulării noastre personalizate, am scris o bibliotecă numită Este o bibliotecă mică și ușoară care înfășoară API-ul Fetch și oferă o modalitate simplă și declarativă de a gestiona erorile, retrișele, timelapse, anulările și câteva alte caracteristici. ffetch Să rescriem logica fetch folosind Puteți găsi codul de pe ramură a repo-ului, astfel încât să o puteți verifica cu . ffetch ffetch git checkout ffetch În primul rând, instalarea În Folderul : ffetch frontend npm install @gkoos/ffetch Putem să ne reîntoarcem la Fișierul utilizat : 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; } } Codul este mult mai curat și mai ușor de citit.Nu mai trebuie să ne facem griji cu privire la revizuiri, timelapse sau anulări. Creăm doar un client cu opțiunile dorite și îl folosim pentru a face solicitări. ffetch Alte beneficii ale utilizării ffetch Interruptor de circuit: răcire automată a punctului final după eșecuri repetate Backoff automat exponențial pentru retrieuri: creșterea timpului de așteptare între retrieuri Gestionarea globală a erorilor: hooks pentru înregistrare, modificarea solicitărilor/răspunsurilor etc. De exemplu, putem alege să reparăm erorile de rețea și erorile serverului 5xx, dar nu și erorile clientului 4xx. nu face nimic magic pe care nu l-ați putea construi singur, dar vă salvează de la scrierea, testarea și întreținerea tuturor acestor boilerplate. Este un înveliș convenabil care se coace în modele de nivel de producție (cum ar fi circuitul de rupere și backoff), astfel încât să vă puteți concentra pe aplicația dvs., nu pe logica dvs. de colectare. De asemenea, se oprește la stratul de colectare, astfel încât să puteți utiliza în continuare propriile dvs. cache, management de stat și biblioteci UI după cum doriți. ffetch Concluzie Principiul principal al acestui articol nu este că ar trebui să utilizați în mod specific, dar că nu ar trebui să se bazeze pe vanilie Fetch pentru aplicații gata de producție. Rețeaua este nesigură, și trebuie să fiți pregătiți pentru ea. Trebuie să se ocupe de erori, retries, timeout-uri, anulări, și caching grațios pentru a oferi o experiență de utilizator bună. ffetch Exact ceea ce trebuie să faceți depinde de cazul și cerințele dvs. specifice de utilizare, dar nu puteți merge la producție pentru a gestiona doar calea fericită. Poate ajuta cu asta. ffetch