Načítání ve stylu SWR s XSstate

V tomto příspěvku použijeme knihovnu XState k vytvoření stavového stroje, který implementuje nástroj pro získávání dat zatuchlý-během-znovuvalidace s automatickým obnovením, když jsou data zastaralá.

Jednoduchý příklad hotového produktu můžete najít na této ukázkové webové stránce.

Co je SWR a proč je užitečné? 🔗

Stale-while-revalidate , neboli SWR, je strategie načítání dat, která umožňuje, aby se data uložená v mezipaměti zobrazila uživateli co nejdříve a zároveň zajistila načtení nejnovějších dat, pokud je verze uložená v mezipaměti zastaralá. Nejčastěji se vyskytuje u mezipamětí HTTP, react-query a SWR Knihovny React usnadnily implementaci takových strategií do frontendu.

Zobrazení nejnovějších dat 🔗

Moderní webové aplikace tráví spoustu času načítáním dat k zobrazení uživateli. Po načtení se však data automaticky neaktualizují, i když se mezitím změnila. To není vždy důležité, ale může to být důležité pro uživatelský dojem.

Představte si aplikaci pro analýzu dat se seznamem sestav, které lze spouštět z různých míst aplikace. Pokud aplikaci používáme a jiný spoluhráč přidá nebo odebere zprávu, měla by se tato změna projevit u všech ostatních, aniž by bylo nutné kartu znovu načítat.

Některá řešení používají websockets, události odeslané serverem nebo jiné technologie k přenosu změn ze serveru do prohlížeče v reálném čase. Tato řešení však mohou aplikaci přidat značné množství složitosti a problémů s škálováním, přičemž ve většině případů mají jen malý přínos.

SWR má mnohem jednodušší strategii. Náš stavový automat bude pravidelně načítat nová data, dokud o ně něco v aplikaci stále má zájem. Některé další implementace SWR fungují spíše jako proxy, čekají na skutečné požadavky na data a poté se rozhodují, kdy načíst aktualizovaná data a kdy přejít do mezipaměti.

Volba mezi těmito dvěma styly načítání SWR závisí na povaze vaší aplikace a každé konkrétní části dat a také na tom, jaký typ kódu skutečně spotřebovává data z načítání. Obvykle používám obchody Svelte. Usnadňují rozpoznání, kdy něco poslouchá data obchodu, takže automatické pravidelné obnovování dává největší smysl.

Lepší chování při počátečním zatížení 🔗

První načtení dat představuje menší, ale stále důležitou výzvu. Některé weby používají vykreslování na straně serveru (SSR) ke snížení latence tím, že na serveru poskládají celou úvodní stránku.

Ale to není vždy skvělé řešení. Vytvoření počátečních dat pro načítanou stránku může chvíli trvat, nebo možná používaný webový rámec nepodporuje SSR. Po načtení aplikace ve stylu SPA je SSR samozřejmě zcela bez zapojení.

Když se uživatel přepne na novou stránku, existují tři možnosti:

  • Během načítání dat nedělejte nic (nebo zobrazte indikátor načítání), a jakmile data dorazí, přepněte stránky.
  • Ihned přepněte stránky, ale během čekání na data ukažte indikátor načítání.
  • Uložte to, co jsme na stránce zobrazili minule, a načtěte to z místní mezipaměti, zatímco čekáme, až dorazí nová data.

SWR používá tento třetí přístup. Známým příkladem je aplikace Twitter pro iOS. Když jej otevřete nebo přepnete zpět na hlavní zobrazení časové osy odjinud, zobrazí se, na co jste se dívali, a poté na pozadí načte nové tweety. Po načtení se v horní části zobrazí upozornění, že existují nové tweety, na které se můžete podívat.

Chování SWR 🔗

Technika SWR kombinuje tato dvě chování při načítání dat, aby uživateli poskytla příjemný zážitek. Následuje následující sled událostí:

  1. Pokud existují data uložená v místní mezipaměti, vraťte je jako první, aby uživatel hned viděl něco užitečného.
  2. Pokud od načtení dat z místní mezipaměti uplynulo dost času, nazvěte to „zastaralé“ a načtěte data znovu.
  3. Pravidelně načítajte data znovu, jakmile budou zastaralá, dokud je proces SWR aktivní.

Většina knihoven SWR také odkládá načítání, pokud okno prohlížeče není zaostřené nebo je připojení k internetu offline. Vyhnete se tak zbytečnému načítání jen proto, že někdo nechal svůj počítač zapnutý a nečinný. Jakmile bude karta prohlížeče opět aktivní, načte další data, pokud na to bude čas.

Přehled designu 🔗

SWR fetcher bude podporovat tyto funkce:

  • Sledujte „online“ a stav zaměření karty prohlížeče, abyste věděli, kdy pozastavit obnovování. Nechceme se obnovovat, pokud není k dispozici připojení k síti nebo uživatel aplikaci nepoužívá.
  • Klient knihovny může posílat události do stavového stroje, aby označil, že by se právě teď neměl načítat.
    • Uživatel nemusí být přihlášen nebo nemusí mít povoleno vidět konkrétní třídu dat.
    • Možná jsme jen v části aplikace, která tato data nepotřebuje.
  • Doba mezi aktualizacemi je konfigurovatelná.
    • V závislosti na povaze dat může mezi obnoveními uplynout několik sekund, minuta, hodina nebo dokonce den.
    • Pokud je stavový stroj povolen, automaticky znovu načte data, jakmile uplyne zadaná doba.
  • Klient může poskytnout funkci, která je zpočátku volána, aby získala „zastaralá“ data, pokud nějaká existují.
  • Podrobnosti o načítání dat jsou ponechány na klientovi. Jediným požadavkem je, aby funkce načtení vrátila příslib, který se vyřeší na data.
    • Funkce načtení může také vrátit speciálníUNMODIFIED hodnota označující, že nebyla přítomna žádná nová data. K tomu obvykle dojde, když požadavek na načtení používá etagy nebo If-Modified-Since záhlaví a server označí, že se data nezměnila.
  • Načítací nástroj je vybaven funkcí, kterou volá, když dorazí nová data nebo dojde k chybě.
  • Pokud dojde k chybě, načítání se automaticky zopakuje pomocí časovače exponenciálního stažení.

Oblíbené knihovny SWR podporují některé další funkce, které zde nebudeme implementovat:

  • Správa mezipaměti
  • Obsluha více klientů pro konkrétní část dat pomocí jednoho stavového automatu.
  • Funkce stránkovaného/nekonečného „načíst více“.
  • Sloučit nevyřízené mutace dat s posledními daty přijatými ze serveru.

Většinu z těchto funkcí lze přidat navrch, aniž byste museli upravovat stavový stroj načítání, a jejich přidáním se možná budu zabývat v budoucím článku.

Kdy vyzvednout 🔗

Nejprve načítání počká, dokud neuplyne dostatek času od předchozího načtení. Pokud víte, že potřebujete načíst právě teď, můžete mu to přikázat vynuceným obnovením událost.

Dále se ujistíme, že je karta prohlížeče zaměřena a že je k dispozici internet. Nechceme aportovat, pokud tomu nikdo nevěnuje pozornost nebo pokud to stejně selže. Musí být také povolen fetcher. Obvykle to znamená, že uživatel je v části aplikace, která používá data.

Například ve Svelte může být fetcher připojen k obchodu. Když obchod získá prvního odběratele, povolíme načítání, a když se vrátí na nulu odběratelů, načítání znovu deaktivujeme, protože data již nic nepoužívá.

Kromě toho, že je nástroj pro načítání povolen, musí být povolen provozovat. Funguje to podobně jako povoleno nastavení, ale také ignoruje vynutit obnovení událost. Načítání nemusíme povolit, pokud uživatel ještě není přihlášen nebo pokud ještě nemáme nějaké další potřebné informace potřebné ke správnému načítání.

Opakování při chybě 🔗

Když se načtení nezdaří, stavový automat se automaticky pokusí znovu. Používá exponenciální zpětné vypínání, což znamená, že po každém neúspěšném načtení bude čekat dvakrát déle než předchozí pokus.

Může se tedy pokusit opakovat po 1 sekundě, poté po 2 sekundách, pokud stále selhává, pak po 4 sekundách a tak dále. Existuje také maximální doba opakování, abychom na opakování nemuseli čekat hodiny.

Rychlý přehled XSstate 🔗

XState je Javascriptová knihovna pro implementaci Statecharts, což jsou stroje s konečným stavem rozšířené o spoustu užitečných funkcí. Zatímco předchozí články této série se zaměřovaly na implementaci stavových automatů od nuly, pro cokoli složitého považuji XState za skvělý rámec pro sestavení.

Formát konfigurace XSstate je velmi podobný formátu, který jsem popsal ve svých předchozích příspěvcích na blogu o státním stroji. Pokud jste tyto příspěvky nečetli, měli byste si je rychle vyzvednout.

Události 🔗

Události jsou pouze hodnoty odeslané do stavového stroje, aby spustily nějaké chování. Každý stav zpracovává události pomocí své vlastní sady přechodů a akcí a stavový automat může také definovat globální ovladače, které se spustí, pokud aktuální stav událost nezpracovává.

Stroj XSstate má send funkce pro odesílání událostí. Událost může také obsahovat nějaká data a akce spouštěné událostí mohou tato data vidět a správně jednat.

Akce 🔗

Akce jsou jedním ze způsobů interakce stavových automatů se zbytkem systému. Mohou být spuštěny akcemi nebo spuštěny jako součást vstupu do stavu nebo opuštění stavu.

XState má speciální typy akcí k provádění věcí, jako je odesílání událostí nebo aktualizace kontextu stavového stroje. Akce mohou být také jen normální funkce. Pro náš SWR fetcher budou všechny akce buď normální funkce, které volají receive zpětné volání nebo speciální assign akce, které aktualizují vnitřní kontext.

Více podrobností o akcích na Actions | XSstate Docs.

Definice stavů 🔗

Definice stavu definují, jak stavový automat reaguje na události v určitých časech. Stavy v XState mohou také spouštět akce nebo spouštět asynchronní procesy, jako jsou sliby.

Aktuální stav je výstupem stavového automatu. To znamená, že uživatelé stavového automatu mohou vidět, jaký je stav, a založit na tom své vlastní chování.

Kontext státního stroje 🔗

Kontext je pouze libovolná datová struktura spojená se stavovým automatem. Užitečný způsob, jak přemýšlet o kontextu, je ten, že zatímco stavy jsou konečné, kontext je pro nekonečná data. To zahrnuje věci, jako jsou časová razítka, čítače a další související data, která je zdlouhavé nebo nemožné znázornit pouhým stavovým diagramem.

Kontext lze použít ke změně chování stavového automatu a je také viditelný pro uživatele stavového automatu.

Implementace 🔗

Možnosti při vytváření Fetcheru 🔗

Při vytváření fetcheru můžete předat možnosti konfigurace jeho chování:

  • fetcher je funkce, která načítá data. Stavový automat zavolá tuto funkci při každém obnovení.
  • receive je funkce, kterou volá fetcher, když přijal nějaká data nebo narazil na chybu. Efektivně výstup z načítání.
  • initialData je volitelná funkce, která vrací data, která mají být použita, než bude první načtení úspěšné. Pokud je k dispozici, načítací nástroj zavolá tuto funkci při jejím prvním vytvoření. To bude obecně načteno z nějaké mezipaměti.
  • key je hodnota, která je předána do fetcher a initialData funkcí. Načítací nástroj jej jinak nepoužívá.
  • name je řetězec používaný pro výstup ladění. Výchozí hodnota je key není-li poskytnuto.
  • autoRefreshPeriod určuje, jak dlouho čekat před opětovným obnovením dat.
  • maxBackoff je nejdelší doba čekání mezi načtením při opakování po chybách.
  • initialPermitted a initialEnabled označte, zda má být načítání povolen a povolen, když je vytvořen. Výchozí hodnota je true , ale pokud false stavový automat bude čekat, až bude moci načíst příslušné události.

Kontext státního stroje pro získávání 🔗

Náš fetcher zachovává tyto hodnoty v kontextu:

  • lastRefresh záznamy, kdy došlo k předchozí aktualizaci. To nám umožňuje vypočítat, kdy by mělo dojít k dalšímu obnovení.
  • retries je počet, kolikrát se nám nepodařilo načíst a zkusili jsme to znovu.
  • reportedError označuje, zda jsme selhali a nahlásili chybu načítání. Děje se tak, abychom nehlásili stejnou chybu znovu a znovu.
  • storeEnabled , browserEnabled a permitted sledujte, zda má obchod povoleno obnovení. I když jsou také spojeny se stavy v počítači, některé události si mohou vynutit obnovení a pak je užitečné podívat se na tyto příznaky, abyste zjistili, do kterého stavu se po dokončení obnovení vrátit.

Státy 🔗

Přes veškerou tuto výstavní a designérskou práci je skutečný stav automatu poměrně jednoduchý. Existuje pouze šest stavů a ​​určitá podpůrná logika.

možná Start 🔗

Toto je počáteční stav a stavový stroj se do něj také vrátí, kdykoli může potřebovat naplánovat další načtení. Existuje proto, aby ostatní státy mohly přejít sem, aby zjistily, co dělat dál, místo toho, aby všude znovu implementovaly logiku.

V řeči stavových tabulek se stav, který okamžitě přechází do jiného stavu, nazývá stav stavu .

maybeStart: {
  always: [
    { cond: 'not_permitted_to_refresh', target: 'notPermitted' },
    { cond: 'can_enable', target: 'waitingForRefresh' },
    { target: 'disabled' },
  ],
},

always klíč říká XSstate, aby spustil tyto přechody okamžitě, bez čekání na jakoukoli událost nebo zpoždění. Pokud hodnoty v kontextu naznačují, že aktualizace není aktuálně povolena, přejde na notPermitted nebo disabled státy. Pokud je aktualizace povolena právě teď, přejde na waitingToRefresh .

XSstate Guards 🔗

Tyto přechody používají cond klíčové slovo, které označuje podmínku, která musí být pravdivá, aby se přechod spustil. XSstate tyto podmínky nazývá stráže a takto vypadají v konfiguraci našeho státního stroje.

guards: {
    not_permitted_to_refresh: (ctx) => !ctx.permitted,
    permitted_to_refresh: (ctx) => ctx.permitted,
    can_enable: (ctx) => {
      if (!ctx.storeEnabled || !ctx.permitted) {
        return false;
      }

      if (!ctx.lastRefresh) {
        // Refresh if we haven’t loaded any data yet.
        return true;
      }

      // Finally, we can enable if the browser tab is active.
      return ctx.browserEnabled;
    },
  },

Máme dva strážce týkající se toho, zda má stavový automat povoleno se obnovovat nebo ne, a dalšího, který kontroluje všechny podmínky související s tím, zda může načítání naplánovat načítání.

Global Event Handlers 🔗

Všechny obslužné programy globálních událostí stavového stroje aktualizují kontextové informace týkající se toho, zda je načítání povoleno či nikoli, a poté přejdou do maybeStart stát, abyste zjistili, co dělat dál.

Vzhledem k tomu, že tyto ovladače jsou definovány mimo jakýkoli stav, spouštějí se vždy, když aktuální stav nemá vlastní ovladač pro událost.

on: {
    FETCHER_ENABLED: { target: 'maybeStart', actions: 'updateStoreEnabled' },
    SET_PERMITTED: { target: 'maybeStart', actions: 'updatePermitted' },
    BROWSER_ENABLED: {
      target: 'maybeStart',
      actions: 'updateBrowserEnabled',
    },
  },

nepovoleno a zakázáno 🔗

maybeStart stav přejde do těchto stavů, pokud načítání není aktuálně povoleno. V notPermitted stavu se nesmí stát nic kromě obslužných rutin globálních událostí. Tento stav také vymaže informace o posledním obnovení a odešle null data do funkce příjmu.

V disabled Stavový stroj je nečinný, dokud nepřijme události nezbytné k opětovnému naplánování načítání. Klient však může spustit aktualizaci pomocí FORCE_REFRESH událost, i když k obnovení nedojde automaticky.

// Not permitted to refresh, so ignore everything except the global events that might permit us to refresh.
notPermitted: {
  entry: ['clearData', 'clearLastRefresh'],
},
// Store is disabled, but still permitted to refresh so we honor the FORCE_REFRESH event.
disabled: {
  on: {
    FORCE_REFRESH: {
      target: 'refreshing',
      cond: 'permitted_to_refresh',
    },
  },
},

čekání na obnovení 🔗

Když je obnovování povoleno, stavový stroj čeká v waitingForRefresh stav, dokud není čas na obnovení. A FORCE_REFRESH událost může přesto okamžitě spustit aktualizaci.

waitingForRefresh: {
  on: {
    FORCE_REFRESH: 'refreshing',
  },
  after: {
    nextRefreshDelay: 'refreshing',
  },
}

Zpoždění 🔗

after klíč na stavu může definovat chování, které se stane po určité době, pokud nic jiného nezpůsobilo přechod jako první. Jako každý přechod lze i tyto chránit pomocí cond hodnotu, pokud si to přejete.

Zpoždění může být pevné nebo variabilní. Pevné zpoždění má jednoduše hodnotu zpoždění jako klíč.

after: {
  400: 'slowLoading'
}

XState také podporuje dynamické zpoždění, a to je to, co zde používáme. Dynamická zpoždění jsou definována v delays sekce konfigurace stavového stroje a každá funkce zpoždění vrátí počet milisekund, které má čekat. waitingForRefresh stav používá nextRefreshDelay funkce.

delays: {
  nextRefreshDelay: (context) => {
    let timeSinceRefresh = Date.now() - context.lastRefresh;
    let remaining = autoRefreshPeriod - timeSinceRefresh;
    return Math.max(remaining, 0);
  },
  errorBackoffDelay: /* details later */,
},

Samotná funkce je poměrně jednoduchá. Zjišťuje, před jak dlouhou dobou došlo k předchozí aktualizaci a jak dlouho by měla čekat, než je naplánována další aktualizace.

Zejména zpoždění používají setTimeout a všechny hlavní implementace prohlížeče používají k načasování zpoždění 32bitové celé číslo se znaménkem. To znamená, že zpoždění delší než přibližně 24 dní se převalí a způsobí nesprávné chování. Takže pokud opravdu chcete z nějakého důvodu odložit tak dlouho, budete muset vytvořit extra kód, aby to fungovalo.

osvěžující 🔗

refreshing stav volá dodaný fetcher a upozorní klienta, když má nová data.

refreshing: {
  on: {
    // Ignore the events while we're refreshing but still update the
    // context so we know where to go next.
    FETCHER_ENABLED: { target: undefined, actions: 'updateStoreEnabled' },
    SET_PERMITTED: { target: undefined, actions: 'updatePermitted' },
    BROWSER_ENABLED: {
      target: undefined,
      actions: 'updateBrowserEnabled',
    },
  },
  // An XState "service" definition
  invoke: {
    id: 'refresh',
    src: 'refresh',
    onDone: {
      target: 'maybeStart',
      actions: 'refreshDone',
    },
    onError: {
      target: 'errorBackoff',
      actions: 'reportError',
    },
  },
},

Přepsání obslužného programu globálních událostí 🔗

refreshing state definuje obslužné rutiny pro události povolení, které stále volají relevantní akce, ale nemají žádný cíl.

Tímto způsobem se kontext stále aktualizuje, takže maybeStart může příště udělat správnou věc, ale nepřerušíme načítání tím, že opustíme stav příliš brzy, pokud je stavový automat během načítání deaktivován.

Služby XSstate 🔗

XSstate využívá služby provádět asynchronní operace. Existuje několik různých typů služeb:

  • A Promise spustí a poté vyřeší nebo odmítne.
  • Pozorovatelný , jako je ta implementovaná v rxjs knihovny, může odeslat více událostí a poté je skončit.
  • Služba může být také celým stavovým automatem sama o sobě, který komunikuje tam a zpět s aktuálním stavovým automatem. Služba je považována za dokončenou, když vyvolaný stroj vstoupí do svého konečného stavu.

invoke objekt ve stavu definuje službu. Jeho src klíč označuje službu, která se má vyvolat, a v závislosti na typu služby onDone a onError definovat další přechody a akce, které je třeba provést.

Zde používáme pouze jednu službu, která volá fetcher funkci dodanou klientem a vrátí svůj slib.

services: {
  refresh: () => fetcher(key),
},

Zpracování výsledku 🔗

Zpracovatelé výsledků jsou relativně jednoduché.

Když je načtení úspěšné, stavový automat provede refreshDone akci a poté se vrátí na maybeStart abyste zjistili, co dělat dál.

onDone: {
  target: 'maybeStart',
  actions: 'refreshDone',
},

refreshDone akce zaznamená, kdy došlo k obnovení, vymaže informace o opakování a poté zavolá receive zpětné volání. To se provádí jako assign akce, takže jeho návratová hodnota je sloučena s existujícím kontextem.

refreshDone: assign((context, event) => {
  let lastRefresh = Date.now();
  let updated = {
    lastRefresh,
    retries: 0,
    reportedError: false,
  };

  if(event.data !== UNMODIFIED && context.permitted) {
    receive({ data: event.data, timestamp: lastRefresh });
  }

  return updated;
})

Pokud načtení vrátí chybu, zaznamenáme to a připravíme se na další pokus. errorBackoff stav, popsaný níže, zpracovává čekání na další opakování.

onError: {
  target: 'errorBackoff',
  actions: 'reportError',
},

reportError akce upozorní klienta, pokud tak již neučinil.

reportError: assign((context: Context, event) => {
  // Ignore the error if it happened because the browser went offline while fetching.
  // Otherwise report it.
  if (
    !context.reportedError &&
    browserStateModule.isOnline() // See the Github repo for this function
  ) {
    receive({ error: event.data });
  }
  return {
    reportedError: true,
  };
}),

errorBackoff 🔗

Když se načtení nezdaří, stavový stroj vstoupí do chybového backoff stavu, který čeká na další pokus s delším zpožděním při každém opakování.

errorBackoff: {
  entry: ‘incrementRetry’,
  after: {
    errorBackoffDelay: ‘refreshing’,
  },
},

incrementRetry jen přidá jeden k počtu opakování:

incrementRetry: assign({ retries: (context) => context.retries + 1 }),

A errorBackoffDelay funkce vypočítá, jak dlouho čekat pomocí algoritmu exponenciálního backoff:

delays: {
  errorBackoffDelay: (context, event) => {
    const baseDelay = 200;
    const delay = baseDelay * (2 ** context.retries);
    return Math.min(delay, maxBackoff);
  },
}

Použití v aplikaci 🔗

Tento fetcher můžete použít přímo v komponentě a mít receive zpětné volání aktualizuje příslušný stav komponenty. U dat sdílených mezi komponentami obvykle zabalím fetcher do úložiště Svelte, které vypadá zhruba jako tento příklad:

import { writable } from 'svelte/store';

export function autoFetchStore({url, interval, initialDataFn}) {
  var store = writable({}, () => {
    // When we get our first subscriber, enable the store.
    f.setEnabled(true);
    // Then disable it when we go back to zero subscribers.
    return () => f.setEnabled(false);
  });

  var f = fetcher({
    key: url,
    autoRefreshPeriod: interval,
    fetcher: () => fetch(url).then((r) => r.json()),
    receive: store.set,
    initialData: initialDataFn,
    initialEnabled: false,
  });

  return {
    subscribe: store.subscribe,
    destroy: f.destroy,
    refresh: f.refresh,
  };
}

A tak to je! To, co mohlo být složitým kouskem kódu se spoustou nešikovných bitů a podmínek, je docela jednoduché a snadno pochopitelné, když je implementováno jako stavový stroj.

Úplnou verzi kódu najdete zde v tomto úložišti Github.

V XState je podporováno mnohem více funkcí, které jsem zde nepopsal. Kromě jiných skvělých funkcí můžete mít hierarchie stavů, paralelní nebo vnořené stavové stroje a udržovat historii stavu.

Sledujte tento web nebo mě sledujte na Twitteru, abyste viděli, až zveřejním svůj další článek o stavových automatech:jak otestovat stavové automaty, jako je tento, aniž byste se zbláznili!