Използвайки остарял, докато се презаверява HTTP Cache-Control
удължаването е популярна техника. Включва използването на кеширани (остарели) активи, ако са намерени в кеша, и след това повторно валидиране на кеша и актуализирането му с по-нова версия на актива, ако е необходимо. Оттук и името stale-while-revalidate
.
stale-while-revalidate
Върши работаКогато заявка се изпраща за първи път, тя се кешира от браузъра. След това, когато същата заявка се изпраща втори път, кешът се проверява първо. Ако кешът на тази заявка е наличен и валиден, кешът се връща като отговор. След това кешът се проверява за остарялост и се актуализира, ако се намери остарял. The остарялост на кеша се определя от max-age
стойност, присъстваща в Cache-Control
заглавка заедно с stale-while-revalidate
.
Това позволява бързо зареждане на страници , тъй като кешираните активи вече не са в критичния път. Те се зареждат моментално. Освен това, тъй като разработчиците контролират колко често кешът се използва и актуализира, те могат да попречат на браузърите да показват на потребителите прекалено остарели данни.
Читателите може би си мислят, че ако могат да накарат сървъра да използва определени заглавки в отговорите си и да остави браузъра да го вземе от там, тогава каква е необходимостта да се използва Реагирайте и куки за кеширане?
Оказва се, че подходът сървър и браузър работи добре само когато искаме да кешираме статично съдържание. Ами използването на stale-while-revalidate
за динамичен API? Трудно е да се измислят добри стойности за max-age
и stale-while-revalidate
в този случай. Често анулирането на кеша и извличането на нов отговор всеки път, когато се изпраща заявка, ще бъде най-добрият вариант. Това на практика означава никакво кеширане. Но с React и Куки , можем да се справим по-добре.
stale-while-revalidate
за APIЗабелязахме, че HTTP’s stale-while-revalidate
не работи добре с динамични заявки като API повиквания.
Дори ако в крайна сметка го използваме, браузърът ще върне кеша или новия отговор, а не и двете. Това не върви добре с заявка за API, тъй като бихме искали нови отговори всеки път, когато се изпраща заявка. Изчакването на нови отговори обаче забавя значимата използваемост на приложението.
И така, какво правим?
Ние прилагаме персонализиран механизъм за кеширане. В рамките на това ние измисляме начин да върнем кеша и новия отговор. В потребителския интерфейс кешираният отговор се заменя с нов отговор, когато е наличен. Ето как би изглеждала логиката:
Този подход позволява незабавни актуализации на потребителския интерфейс - тъй като всяка заявка за API се кешира, но също така и евентуална коректност в потребителския интерфейс, тъй като новите данни за отговор се показват веднага щом са налични.
В този урок ще видим стъпка по стъпка подход за това как да се приложи това. Ще наречем този подход остарял докато се опреснява тъй като потребителският интерфейс всъщност е освежен когато получи свеж отговор.
За да стартираме този урок, първо ще се нуждаем от API, откъдето извличаме данни. За щастие има един тон на фалшиви API услуги. За този урок ще използваме reqres.in .
Данните, които извличаме, са списък с потребители с page
параметър на заявката. Ето как изглежда извличането на кода:
fetch('https://reqres.in/api/users?page=2') .then(res => res.json()) .then(json => { console.log(json); });
Изпълнението на този код ни дава следния изход. Ето неповторима версия на него:
{ page: 2, per_page: 6, total: 12, total_pages: 2, data: [ { id: 7, email: ' [email protected] ', first_name: 'Michael', last_name: 'Lawson', avatar: 'https://s3.amazonaws.com/uifaces/faces/twitter/follettkyle/128.jpg' }, // 5 more items ] }
Можете да видите, че това е като истински API. В отговора имаме пагинация. page
параметърът на заявката е отговорен за промяната на страницата и имаме общо две страници в набора от данни.
Нека да видим как използваме API в React App. След като знаем как да го направим, ще разберем кеширащата част. Ще използваме клас, за да създадем нашия компонент. Ето кода:
import React from 'react'; import PropTypes from 'prop-types'; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { fetch(`https://reqres.in/api/users?page=${this.props.page}`) .then(res => res.json()) .then(json => { this.setState({ users: json.data }); }); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const users = this.state.users.map(user => ( {user.first_name} {user.last_name}
)); return {users} ; } } Component.propTypes = { page: PropTypes.number.isRequired };
Забележете, че получаваме page
стойност чрез props
, както често се случва в реални приложения. Също така имаме componentDidUpdate
функция, която извлича данните от API всеки път this.props.page
промени.
В този момент той показва списък с шест потребители, защото API връща шест елемента на страница:
Ако искаме да добавим към това кеширане при остаряло време за опресняване, трябва да актуализираме логиката на приложението си до:
Можем да направим това, като имаме глобален CACHE
обект, който съхранява кеша уникално. За уникалност можем да използваме this.props.page
стойност като ключ в нашия CACHE
обект. След това просто кодираме алгоритъма, споменат по-горе.
import apiFetch from './apiFetch'; const CACHE = {}; export default class Component extends React.Component { state = { users: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ users: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/users?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ users: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { // same render code as above } }
Тъй като кешът се връща веднага щом бъде намерен и тъй като новите данни за отговор се връщат от setState
това също означава, че имаме безпроблемни актуализации на потребителския интерфейс и няма повече време за изчакване в приложението от втората заявка нататък. Това е перфектно и накратко е методът на остаряло време за опресняване.
apiFetch
функцията тук не е нищо друго освен обвивка над fetch
така че да можем да видим предимството на кеширането в реално време. Това се прави чрез добавяне на случаен потребител към списъка с users
върнати от заявката за API. Той също така добавя случайно забавяне към него:
export default async function apiFetch(...args) { await delay(Math.ceil(400 + Math.random() * 300)); const res = await fetch(...args); const json = await res.json(); json.data.push(getFakeUser()); return json; } function delay(ms) { return new Promise(resolve => setTimeout(resolve, ms)); }
getFakeUser()
тук е отговорна за създаването на фалшив потребителски обект.
С тези промени нашият API е по-реален от преди.
Като се има предвид това, когато променяме page
prop предаден на Component
от нашия основен компонент можем да видим API кеширането в действие. Опитайте да щракнете върху Превключване бутон веднъж на всеки няколко секунди този CodeSandbox и трябва да видите поведение като това:
Ако се вгледате внимателно, се случват няколко неща.
Това е, кешираното остаряло време за опресняване, което търсихме. Но този подход страда от проблем с дублирането на код. Нека да видим как ще стане, ако имаме друг компонент за извличане на данни с кеширане. Този компонент показва елементите по различен начин от нашия първи компонент.
Можем да направим това, като просто копираме логиката от първия компонент. Нашият втори компонент показва списък с котки:
const CACHE = {}; export default class Component2 extends React.Component { state = { cats: [] }; componentDidMount() { this.load(); } load() { if (CACHE[this.props.page] !== undefined) { this.setState({ cats: CACHE[this.props.page] }); } apiFetch(`https://reqres.in/api/cats?page=${this.props.page}`).then( json => { CACHE[this.props.page] = json.data; this.setState({ cats: json.data }); } ); } componentDidUpdate(prevProps) { if (prevProps.page !== this.props.page) { this.load(); } } render() { const cats = this.state.cats.map(cat => ( {cat.name} (born {cat.year})
)); return {cats} ; } }
Както можете да видите, логиката на компонентите, включена тук, е почти същата като първия компонент. Единствената разлика е в заявената крайна точка и че тя показва елементите от списъка по различен начин.
Сега показваме двата компонента един до друг. Можеш да видиш те се държат подобно :
За да постигнем този резултат, трябваше да направим много дублиране на код. Ако имахме няколко компонента като този, щяхме да дублираме твърде много код.
За да го решим по недублиращ се начин, можем да имаме компонент от по-висок ред за извличане и кеширане на данни и предаването им като подпори. Не е идеално, но ще работи. Но ако трябваше да направим множество заявки в един компонент, наличието на множество компоненти от по-висок ред би станало грозно наистина бързо.
След това имаме образа на render props, което е може би най-добрият начин да направите това в компонентите на класа. Работи перфектно, но след това отново е склонен към „ада на обвивката“ и изисква от нас да свързваме текущия контекст понякога. Това не е страхотно изживяване за разработчици и може да доведе до разочарование и грешки.
Това е мястото, където React Hooks спасяват деня. Те ни позволяват да боксираме логиката на компонентите в контейнер за многократна употреба, така че да можем да я използваме на множество места. Реагирайте куки са въведени в React 16.8 и те работят само с функционални компоненти. Преди да стигнем до контрола на кеша на React - по-специално кеширане на съдържание с Hooks - нека първо видим как правим просто извличане на данни във функционални компоненти.
За да извлечем API данни във функционални компоненти, използваме useState
и useEffect
куки.
useState
е аналог на компонентите на класа ’state
и setState
. Използваме тази кука, за да имаме атомни контейнери със състояние във функционален компонент.
useEffect
е кука за жизнения цикъл и можете да го възприемате като комбинация от componentDidMount
, componentDidUpdate
и componentWillUnmount
. Вторият параметър, предаден на useEffect
се нарича масив от зависимости. Когато масивът на зависимостите се промени, обратното повикване се предава като първи аргумент на useEffect
се изпълнява отново.
Ето как ще използваме тези куки за реализиране на извличане на данни:
import React, { useState, useEffect } from 'react'; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { fetch(`https://reqres.in/api/users?page=${page}`) .then(res => res.json()) .then(json => { setUsers(json.data); }); }, [page]); const usersDOM = users.map(user => ( {user.first_name} {user.last_name}
)); return {usersDOM} ; }
Като посочите page
като зависимост от useEffect
, ние инструктираме React да изпълнява обратното ни извикване useEffect всеки път page
се променя. Това е точно като componentDidUpdate
. Също така, useEffect
винаги се изпълнява за първи път, така че работи като componentDidMount
също.
Знаем, че useEffect
е подобен на методите на жизнения цикъл на компонентите. Така че можем да модифицираме функцията за обратно извикване, предадена към нея, за да създадем кеширането при остаряло време за опресняване, което имахме в компонентите на класа. Всичко остава същото, освен useEffect
кука.
const CACHE = {}; export default function Component({ page }) { const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]); // ... create usersDOM from users return {usersDOM} ; }
По този начин имаме кеширане при застой при опресняване, работещо във функционален компонент.
Можем да направим същото за втория компонент, тоест да го преобразуваме, за да функционира и да приложи кеширане при остаряло време за опресняване. Резултатът ще бъде идентично с това, което имахме в класовете.
Но това не е по-добре от компонентите на класа, нали? Така че нека да видим как можем да използваме силата на персонализирана кука, за да създадем модулна логика за остаряло време за опресняване, която можем да използваме в множество компоненти.
Първо, нека стесним логиката, която искаме да преминем към персонализирана кука. Ако погледнете предишния код, знаете, че това е useState
и useEffect
част. По-конкретно, това е логиката, която искаме да модулираме.
const [users, setUsers] = useState([]); useEffect(() => { if (CACHE[page] !== undefined) { setUsers(CACHE[page]); } apiFetch(`https://reqres.in/api/users?page=${page}`).then(json => { CACHE[page] = json.data; setUsers(json.data); }); }, [page]);
Тъй като трябва да го направим общ, ще трябва да направим URL динамичен. Така че трябва да имаме url
като аргумент. Ще трябва да актуализираме и логиката за кеширане, тъй като множество заявки могат да имат еднакви page
стойност. За щастие, когато page
е включен в URL адреса на крайната точка, той дава уникална стойност за всяка уникална заявка. Така че можем просто да използваме целия URL адрес като ключ за кеширане:
const [data, setData] = useState([]); useEffect(() => { if (CACHE[url] !== undefined) { setData(CACHE[url]); } apiFetch(url).then(json => { CACHE[url] = json.data; setData(json.data); }); }, [url]);
Това е почти всичко. След като го увием във функция, ще имаме своя персонализирана кука. Погледнете по-долу.
const CACHE = {}; export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); }); }, [url]); return data; }
Забележете, че сме добавили още един аргумент, наречен defaultValue
към него. Стойността по подразбиране за извикване на API може да бъде различна, ако използвате тази кука в множество компоненти. Ето защо го направихме адаптивно.
Същото може да се направи и за data
въведете newData
обект. Ако вашата персонализирана кука връща разнообразни данни, може да искате просто да върнете newData
а не newData.data
и се справят с това обхождане от страната на компонента.
Сега, когато имаме своя персонализирана кука, която прави тежкото повдигане на остарялото при опресняване кеширане, ето как го включваме в нашите компоненти. Забележете огромното количество код, което успяхме да намалим. Целият ни компонент сега е само три твърдения. Това е голяма печалба.
import useStaleRefresh from './useStaleRefresh'; export default function Component({ page }) { const users = useStaleRefresh(`https://reqres.in/api/users?page=${page}`, []); const usersDOM = users.map(user => ( {user.first_name} {user.last_name}
)); return {usersDOM} ; }
Можем да направим същото и за втория компонент. Ще изглежда така:
export default function Component2({ page }) { const cats = useStaleRefresh(`https://reqres.in/api/cats?page=${page}`, []); // ... create catsDOM from cats return {catsDOM} ; }
Лесно е да разберем колко шаблонния код можем да спестим, ако използваме тази кука. Кодът също изглежда по-добре. Ако искате да видите цялото приложение в действие, преминете към този CodeSandbox .
useStaleRefresh
Сега, когато разполагаме с основите, можем да добавим още функции към нашата персонализирана кука. Например можем да добавим isLoading
стойност в куката, която е вярна всеки път, когато се изпраща уникална заявка и междувременно нямаме кеш, който да покажем.
Правим това, като имаме отделно състояние за isLoading
и да го настроите според състоянието на куката. Тоест, когато няма налично кеширано уеб съдържание, ние го задаваме на true
, в противен случай го задаваме на false
.
Ето актуализираната кука:
export default function useStaleRefresh(url, defaultValue = []) { const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // cacheID is how a cache is identified against a unique request const cacheID = url; // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data apiFetch(url).then(newData => { CACHE[cacheID] = newData.data; setData(newData.data); setLoading(false); }); }, [url]); return [data, isLoading]; }
Вече можем да използваме новия isLoading
стойност в нашите компоненти.
export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( `https://reqres.in/api/users?page=${page}`, [] ); if (isLoading) { return Loading ; } // ... create usersDOM from users return {usersDOM} ; }
Забележи това с това направено , виждате текст „Зареждане“, когато за първи път се изпраща уникална заявка и липсва кеш.
useStaleRefresh
Поддръжка на всеки async
ФункцияМожем да направим нашата персонализирана кука още по-мощна, като я накараме да поддържа всякакви async
функция, а не просто GET
мрежови заявки. Основната идея зад нея ще остане същата.
Просто свързване на function.name
и arguments
ще работи като кеш ключ за нашия случай на употреба. Използвайки това, ето как ще изглежда нашата кука:
import { useState, useEffect, useRef } from 'react'; import isEqual from 'lodash/isEqual'; const CACHE = {}; export default function useStaleRefresh(fn, args, defaultValue = []) { const prevArgs = useRef(null); const [data, setData] = useState(defaultValue); const [isLoading, setLoading] = useState(true); useEffect(() => { // args is an object so deep compare to rule out false changes if (isEqual(args, prevArgs.current)) { return; } // cacheID is how a cache is identified against a unique request const cacheID = hashArgs(fn.name, ...args); // look in cache and set response if present if (CACHE[cacheID] !== undefined) { setData(CACHE[cacheID]); setLoading(false); } else { // else make sure loading set to true setLoading(true); } // fetch new data fn(...args).then(newData => { CACHE[cacheID] = newData; setData(newData); setLoading(false); }); }, [args, fn]); useEffect(() => { prevArgs.current = args; }); return [data, isLoading]; } function hashArgs(...args) { return args.reduce((acc, arg) => stringify(arg) + ':' + acc, ''); } function stringify(val) { return typeof val === 'object' ? JSON.stringify(val) : String(val); }
Както можете да видите, ние използваме комбинация от име на функция и нейните низови аргументи, за да идентифицираме еднозначно извикване на функция и по този начин да я кешираме. Това работи за нашето просто приложение, но този алгоритъм е склонен към сблъсъци и бавни сравнения. (С несериализируеми аргументи изобщо няма да работи.) Така че за реалните приложения подходящият хеширащ алгоритъм е по-подходящ.
Друго нещо, което трябва да се отбележи тук, е използването на useRef
. useRef
се използва за запазване на данните през целия жизнен цикъл на заграждащия компонент. Тъй като args
е масив - който е обект в JavaScript - всяко повторно изобразяване на компонента с помощта на куката причинява args
референтен указател за промяна. Но args
е част от списъка на зависимостите в нашия първи useEffect
. Така че args
промяната може да направи useEffect
работи дори когато нищо не се е променило. За да се противопоставим на това, правим задълбочено сравнение между стари и настоящи args
използвайки е равно и оставете само useEffect
обратно извикване, ако args
всъщност променен.
Сега можем да използваме този нов useStaleRefresh
кука, както следва. Забележете промяната в defaultValue
тук. Тъй като това е кука с общо предназначение, ние не разчитаме на нашата кука да върне data
ключ в обекта за отговор.
export default function Component({ page }) { const [users, isLoading] = useStaleRefresh( apiFetch, [`https://reqres.in/api/users?page=${page}`], { data: [] } ); if (isLoading) { return Loading ; } const usersDOM = users.data.map(user => ( {user.first_name} {user.last_name}
)); return {usersDOM} ; }
Можете да намерите целия код в този CodeSandbox .
useStaleRefresh
куката, която създадохме в тази статия, е доказателство за концепция, която показва какво е възможно с React Hooks. Опитайте се да играете с кода и да видите дали можете да го впишете в приложението си.
Като алтернатива можете също да опитате да използвате остарялото време за опресняване чрез популярна, добре поддържана библиотека с отворен код като swr или реакция-запитване . И двете са мощни библиотеки и поддържат множество функции, които помагат при заявки за API.
React Hooks са променящи играта. Те ни позволяват елегантно да споделяме логиката на компонентите. Преди това не беше възможно, тъй като състоянието на компонентите, методите на жизнения цикъл и изобразяването бяха пакетирани в едно цяло: компоненти на класа. Сега можем да имаме различни модули за всички тях. Това е чудесно за композиране и писане на по-добър код. Използвам функционални компоненти и куки за всички нови React кодове, които пиша, и горещо препоръчвам това на всички React разработчици.
Остарял кеш е кеш, който не е подходящ за използване, тъй като съдържа остарели данни. В контекста на HTTP това се случва, когато максималната възраст на кеша или s-maxage е изтекла. Това е подобно на начина, по който храната, която се съхранява за дълго време, остарява, следователно терминът „остарял кеш“.
Остарялото съдържание е съдържание, чиято валидност е изтекла. В контекста на CDN, това означава, че съдържанието съществува повече от стойността на времето за живот (TTL), прикрепена към него. Така че не е подходящ за употреба, оттук и терминът „остарял“.
React Hook е функция, която ни позволява да се включим в състоянието и методите на жизнения цикъл на React Function Component. Поради това, Hook ни позволява да пропуснем използването на Class Components, защото чрез него са достъпни методи на жизнения цикъл като componentDidMount, componentDidUpdate и componentWillUnmount.
Cache-Control е HTTP заглавка, която определя кеширане, свързано с заявка. Той може да бъде дефиниран независимо в заглавките на заявки и отговори. С Cache-Control трябва да се посочат директиви като no-cache, must-revalidate и max-age, за да се определи как трябва да се кешира отговорът на заявката.
Директивата за задължително повторно потвърждаване в управлението на кеша на HTTP посочва, че след като кешът е остарял, той не трябва да се използва до по-нататъшно повторно потвърждаване. Повторното потвърждаване се осъществява чрез свързване с първоначалния сървър на кеша и проверка дали има по-нова версия. Ако има по-нова версия, тя се извлича и след това се използва.
Кешираните данни не са строго важни, но е желателно, тъй като подобряват ефективната производителност. Системата може да обслужва кеширани данни незабавно, докато зареждането на нови данни отнема време. Кешираните данни заемат дисково пространство, така че е добре да ги изтриете, ако е необходимо.