Na obsah stránky

Jednoduše a rychle: LinkeDOM

Aleš Roubíček |

Pokud chcete v Node.js pracovat s HTML dokumenty, máte v podstatě dvě možnosti jak to mainstreamově udělat – cheerio a JSDOM. Cheerio je klon jQuery pro Node.js, který najdete třeba jako základ scraperů na platformě Apify. JSDOM je těžkotonážní framework, který se snaží simulovat browser v kontextu Node.js. Většinou se používá pro testování frontentových komponent bez nutnosti spouštět celý browser. Sic.

Obě možnosti považuju za nedobré. Ať už kvůli paměťové náročnosti a obecné neefektivnosti, která se vzahuje na obě jmenovaná řešení, tak nestandartní API, jaké představuje cheerio. Máme nějaké lepší možnosti?

LinkeDOM

Ano. Máme. Je jím knihovna LinkeDOM, která si od začátku klade za cíl být efektivní (jak na paměť, tak cykly procesoru) a mít API velmi blízké DOM standardu. Dosahuje toho díky chytré volbě vhodných datových struktur pro reprezentaci stromové povahy DOMu – triple-linked lists – a nelpění na plné implementaci DOM standardu.

Obecně se dá říct, že většina výkonových problémů dnešních webových aplikací lěží v doméně reprezentace stavu pomocí nevhodných datových struktur a přístupu k nim. Zdravíme všechny poor-man immutability knihovny pro menežování stavu nejen Reactích aplikací. 👋🖕🏼

Ale zpátky k LinkeDOMu. Jak už jsem naznačil jde o knihovnu, která primárně cílí na Node.js. Exportuje však i ESM moduly, které nejsou závislé na internals Node.js a jsou tak použitelné i v kontextu Web Workerů v Browseruv Cloudflare nebo v běhovém prostředí Deno.

Na co se hodí?

Jedním z cílů LinkeDOM je rychlý SSR, ale dá se použít i pro efektivní testování komponent nebo web scraping ve vysokém scale s rozumnými náklady.

Jak ho tedy použít?

Použití je celkem přímočaré – importujeme ES modul linkedom a z něj použijeme funkci parseHTML:

import { parseHTML } from "https://esm.sh/linkedom";

Dále funkci parseHTML předáme vstupní HTML dokument:

const { document, customElements, HTMLElement } = parseHTML(`<!DOCTYPE html>
  <html lang="en">
    <head>
      <title>Hello from Deno</title>
    </head>
    <body>
      <h1>Hello from Deno</h1>
      <form>
        <input name="user">
        <button>
          Submit
        </button>
      </form>
    </body>
  </html>
`);

To nám vrátí „sandbox“ globálního kontextu (aka window), kde najdete podporované části DOM standardu. Super je, že můžete sdílet kód pro práci s DOMem mezi serverem i klientem. Kód, který si můžete odladit v DevTools browseru, bez problémů přenesete na server. Co LinkeDOM nepodporuje, jsou dynamické vlastnosti:

const input = document.querySelector("input[name=user]");
console.log(input.name);

Tohle vám v browseru vypíše "user", ale v LinkeDOM je to undefined. V tomto případě musíte použít metodu getAttribute:

const input = document.querySelector("input[name=user]");
console.log(input.getAttribute("name"));

Vzhledem k tomu, že všechny atributy stejně nejsou dynamické ani v browseru, je lepší jít ukecěnajší cestou tak jako tak. Už kvůli konzistenci.

Osobně ve svých scriptech a actorech už nepoužívám nic jinýho. Existující actory Hlídače shopů postupně na LinkeDOM překlápíme při nutné údržbě, kvůli změnám sledovaných webů. Kód je pak pro nás přehlednější při údržbě a efektivnější v provozu. A to se vyplatí!

Pod Vocasem

Aleš Roubíček |

Nedávno mě Džoukr oslovil, zda bych jim neudělal kašpárka v jejich podcastu Pod Vocasem. Konkrétně, zda bychom si mohli popovídat o Clojure. Pokud jde o příležitost popovídat si s Románkem osobně, tak to jsem si nemohl nechat ujít. No a celé si to můžete poslechnout tady:

Google Analytics bez cookies

Aleš Roubíček |

Máme tu rok 2022 a weby jsou plné “cookie lišt“ a jiných podivných popupů. Ačkoliv nutně nemusí. Ne, že by nemuseli uživatele informovat o tom, že se ho chystají špehovat na každém kroku – a tato data prodávat dál. To je samozřejmě k uživateli fér. Nemusí ale uživatele za každou cenu špiclovat.

Já vím. Všichni si ale chceme honit pindíky nad erektivními grafy v Google Analytics. I mě zajímá, kolik lidí si tento článek přečte. Nechci u toho pomáhat Googlu k získávání dat, která mu na mým webu můžou být ukradená… Jak vidíte, žádnou cookie lištu nevidíte!

Není to tím, že bych se na ní vykašlal. Místo zhoršování UX, jsem se rozhodl, že mi stačí jednoduchá analytika. Koukal jsem, že na Twitteru je to žhavé téma a nikdo neví pořádně jak na to. A když, tak na to plánuje prodávat školení nebo já nevím co. Přitom je to taková prkotina…

Základem je neřešit kód, co vám vygenerují GA, protože ten asi tak snadno nenakonfigurujete. A pořád vkládáte jejich script do stránky a to je to, co nechceme. Analytics mají své API. Můžeme si i rozšířit svou znalost webové platformy o zajímavé API pro asynchroní zasílání analytických dat navigator.sen­dBeacon:

<script>
   self._ga = {
     // Full Measurement Protocol param reference:
     // https://developers.google.com/analytics/devguides/collection/protocol/v1/parameters
     data: {
       v: "1", // Measurement Protocol version.
       tid: "UA-TVUJ-TRACKING-KOD", // Tracking ID.
       cid: `${Date.now()}${Math.random()}`, // Client ID.
       dl: location.href, // Document location.
       aip: 1, // Anonymize IP
     },
     send(additionalParams) {
       navigator.sendBeacon(
         "https://google-analytics.com/collect",
         new URLSearchParams({
           ...this.data,
           ...additionalParams,
         }).toString()
       );
     },
   };
   _ga.send({ t: "pageview" });
 </script>

Nahraďte UA-TVUJ-TRACKING-KOD za kód, který vám analytics vygenerují třeba pro použití v GTM (nebo je někde v původním tracking code).

Zajímavým plusem tohoto řešení je, že vám ze statistik zmizí IE11 a starší, které Beacon API nemají. Takže ho pak můžete přestat s klidným srdcem řešit. A to se vyplatí!

Pokud používáte GA4, tak tenhle kód vám fungovat nebude. Ale taky má API! Těžko říct, zda je použitelné.

Kód jsem sám nevymýšlel, ale okopíroval jsem jej od bývalého zaměstnance Googlu – Surmy.

Užívejte!

Jednoduše a rychle: lit-html

Aleš Roubíček |

Přiznejme si to. Dnešní stav tvorby webových frontendů není jednoduchý a začít novou věc vyžaduje spoustu kroků, které dříve nebyly potřeba a považovalo se to za výhodu webu. Dneska musíte znát Reacty, Babely, TypeScripty, Webpacky, CRAppy a co já vím, abyste vůbec vytvořili Hello World. Nebo aspoň si to velká část Frontend vývojářů asi myslí, že je to dobrý nápad, a tak webu lámou nožičky.

Nejen, že je takový přístup drahý na vývoj i údržbu, ale hlavně je to nesmysl. Já vím, velké enterprise systémy mají jiné požadavky… Je pak fajn, když za námi chodí zákazníci s přesně takovými systémy, že by potřebovali něco malého, rychlého a snadno použitelného, protože u nich to nejde.

No, ale dneska se pojďme raději věnovat něčemu jinému – knihovně Lit 2.0, která vám umožní psát jako v Reactu, ale bez všech těch zbytečných kejklí okolo. Tato knihovna vznikla v Google. V teamu, který tvořil framework Polymer. Knihovna využívá vlastnosti moderní webové platformy. Neřeší tak dávno neexistující problémy jako například nástroje, které jsem už jmenoval. Díky tomu může být malá (~5 KiB gzip), rychlá a dá se snadno komponovat s dalšími knihovnami, abyste mohli tvořit systémy, které řeší vaše business problémy. Nikoliv problémy špatných komplikovaných frameworků a nástrojů.

Knihovna Lit se historicky skládá ze dvou částí – lit-html a LitElement. Dnes se budeme věnovat té první.

lit-html

Knihovna lit-html má na starosti renderování šablon. Představte si React, který má jen čisté funkce jako komponenty, a místo JSX, který vyžaduje kompilační krok, používá nativní feature ECMAScriptu 2015 – tagované šablony.

Jako ukázku užití si vezmeme dynamické vkládání nekritických stylů do stránky:

<script type="module">
    import {render, html} from "https://unpkg.com/lit-html@2.0.2/lit-html.js?module";

    render(html`<link rel="stylesheet" href="/assets/main.css">`, document.head);
</script>

První věc, které si můžete všimnout, je použití ESM (ECMAScript Modules) v HTML. Jakmile máte podporu ESM modulů v browseru (je tam už asi 3 roky), máte většinu moderních vlastností ECMAScriptu k dispozici a nemusíte řešit transpilaci nebo polyfilly. A tak můžeme směle importovat!

Využijeme službu unpkg.com, která slouží jako CDN pro distribuci npm balíčků s možností transformace node_modules resolution na unpkg URLs – k tomu slouží GET parametr module.

Z balíčku si vyzobneme funkci render a funkci html, která slouží k tagování textových literálů. Funkce render bere jako první parametr šablonu, jako druhý parametr, kam se má vyrenderovat, a třetí, nepovinný, parametr umožňuje přesněji specifikovat, jak se má vyrenderovaný kód vkládat.

Základní chování je takové, že se první render šablony připojí do cílového elementu na konec. Další volání render už tento kus přepisuje. Tohle je jediná breaking change oproti lit-html 1.0, které vždy obsah elementu přepisovalo.

Tahat skoro 3 KiB závislostí jenom kvůli hezkému vložení stylopisu je samozřejmě nesmysl, ale je to pěkná ukázka mnoha zajímavých vlastností. Sami si to můžete hned vyzkoušet ve svém browseru, stačí vám jen DevTools, které tam už jsou. Zápis šablon je úplně přirozený. Nemusíte se učit žádnou novou speciální syntaxi, kterou byste museli kompilovat, stačí vám HTML a ECMAScript. Tak, jak v browserech jsou už roky.

Všimnout si můžete také toho, že funkci render nemusíte předávat žádnou komponentu, ale prostě kus šablony. Přidávat abstrakce můžete, až když je to potřeba. Jestli vůbec taková situace nastane. Stejně tak můžete začít s inline scripty ve stránkách a extrahovat je do samostatných souborů, až když potřebujete sdílet netriviální množství kódu mezi různými stránkami. Já třeba takto dělal web a PWA Hlídače shopů.

Šablonování

Tagované šablony umí interpolaci výrazů a proměnných, což nám umožňuje vytvářet opravdové šablony, do kterých se vkládají data.

export function resultsEmbed(url) {
  const parameters = new URLSearchParams({ url, view: "embed" });
  return html`
    <iframe
      sandbox="allow-same-origin allow-scripts allow-top-navigation allow-popups"
      class="hs-result__embed"
      src="/app/?${parameters}"
    ></iframe>
  `;
}

Tady třeba generujeme iframe s výsledky Hlídače. Pro naformátování GET parametrů použijeme užitečnou třídu URLSearchParams, která se postará o správné encodování hodnot parametrů. Ano, není to tak chytré jako Latte, které pozná samo, jak má vstupy encodovat, ale pořád máme po ruce všechna potřebná primitiva jazyka/platformy. Bez komplikací, bez magie.

Zajímavostí je, že v předchozí verzi 1.0 se u parameters muselo explicitně zavolat toString()/valueOf(). Ve verzi 2.0 se to chová, jak byste u interpolace proměnných v šabloně čekali.

Direktivy

Lit používá pro obohacení chování šablon speciální funkce – direktivy. Například, pokud používáte data, která implementují rozhraní Iterable, nemusíte nad nimi volat Array.from, abyste mohli použít metodu map pro mapování jednotlivých prvků. Můžete místo toho použít direktivu repeat:

export function shopsListTemplate(shops) {
  return html`
    <ul class="hs-shops-list">
      ${repeat(
        shops,
        ({ name, url }) => html`
          <li class="hs-shops-list__item">
            <a href="${url}">${name}</a>
          </li>
        `
      )}
    </ul>
  `;
}

V knihovně je zhruba tucet užitečných direktiv a zároveň základní třída pro implementaci vlastních direktiv.

Závěrem

Lit-html 2.0 je jednoduchá a malá knihovna, která nám umožňuje šablonovat HTML pomocí čistého ECMAScriptu bez potřeby jakýchkoliv kompilátorů. Zkrátka, otevřete browser, DevTools napojíte na lokální HTML soubor a můžete vyvíjet živou aplikaci (REPL style). Já ji používám jako náhradu jak jQuery, tak Reactu. Problémy, které ve své době řešili, jsou už pryč a není třeba s nimi uživatele mučit.

Pokud chcete z vašich šablon udělat znovupoužitelné komponenty, můžete použít další část knihovny Lit – LitElement, na který se podíváme někdy příště. Prozradím jen, že jde o základní třídu pro psaní Web Components, která využívá lit-html pro šablonování a zjednodušuje API, které je podobné základní třídě React.Component. Jen je to malé (zhruba jako Preact), jednoduché a rychlé.

Pulumi a esbuild

Aleš Roubíček |

Poslední tři měsíce jsem z velké části, stejně jako minulý rok, pracoval na Hlídači Shopů. Hodně úsilí jsme věnoval splácení technického dluhu, abychom mohli Hlídače snáze a rychleji (tj. levněji) rozšiřovat. Vytvořil jsem integrační a deployment pipeline, včetně popisu většiny infrastruktury pomocí IaC nástroje Pulumi. Dalším cílem bylo sjednocení code base extenze a webu, aby se daly snadno sdílet části logiky.

V první fázi bylo důležité mít co nejrychleji zdokumentovanou infrastrukturu a být schopní ji automatizovaně rozvíjet. Proto jsem zvolil integrovaný přístup, kdy aplikační a infrastrukturní kód jsou jedna code base, a Pulumi, dík své chytristice, rozhodne, kde se kód řízne a co se nasadí. Takže to byl takový TypeScriptový (dále TS) monolit.

Už během migrace původních lambdiček do Pulumi jsem narážel na takové to svědění, způsobené typovým sebeklamem. Často jsem musel uspokojovat kompilátor, kvůli jeho neschopné typové inferenci, nebo vyloženému ničení explicitně deklarovaných typů. Děkuju pěkně, ale mám lepší představy, jak bych mohl trávit svůj čas. Když pominu, že mám během kompilace TS víc času na čtení twitteru, tak jeho jedinou přidanou hodnotou je popis schémat, na který je vyloženě špatným nástrojem…

AWS vydalo novou verzi JS SDK – verzi třetí. Ta si klade za cíl modularizovat všeobjímající God knihovnu aws-sdk, na sadu specializovaných balíčků, které reflektují stav moderního JavaScriptu, takže obsahují plnohodnotné ESM s moderní syntaxí, ne ty hybridní blbiny pro webpacky v ES5, s kterýma se nedá nic jiného dělat, než nad tou hromádkou neštěstí brečet. Ano, nové AWS SDK obsahuje i plnohodnotné (ehm) TS soubory, protože je v nich napsáno. V praxi se však ukázalo, že je to cesta do pekla…

AWS oznámilo podporu Docker images pro nasazování AWS Lambda funkcí. Chvíli jsem uvažoval, že využiju schopnosti Dockeru mít popis build i runtime kontejnerů v jednom souboru a že každá lambda si tak snadno připraví (pomocí tsc) svoje výstupy a ty se pak nasadí do runtime jako jeden bundle…

O tom, že esbuild je pekelně rychlý a jednoduchý nástroj na kompilaci moderního JavaScriptu (aka ECMAScript 2020), jsem psal minule. Mimochodem umí stejně rychle konzumovat JSX, TS a TSX. Navíc má pěkné API použitelné i z JavaScriptu!

Pulumi má virtualizované balíčkování kódu pro deployment AWS Lambda funkcí. Krom toho, že si můžete nechat transparentně serializovat část kódu v podobě funkce, můžete si také nechat zabalit třeba adresář s kódem, který chcete nasadit. Ale ten už musí být nějak předpřipravený. V rámci virtualizace, ale dojdete až na úroveň, kde do archívu můžete strkat jakýkoliv string, nebo jeho Promise. To je krása opravdových abstrakcí…

JS API esbuildu umí, když mu nastavíte write: false, vracet výsledek bundlování jako, ta-dá, Promise of string. Takže, když si spustíme build service – kvůli optimalizacím na sdílený kód – a při vytváření popisu infrastruktury si necháme také připravit bundle dané funkce, můžeme ho rovnou nasadit. No, a protože to je esbuild a ne kombinace tsc a pulumi, tak je to rychlý nejen v build time, ale i v runtime. A to už se vyplatí!

Více o dopadech na latence jsem sepsal do vlákna na Twitteru, kde najdete i patřičné grafy.

PS: Na zrychlení má podíl nejen menší bundle, ale i nové AWS SDK, které k tomu zmenšení také přispívá, ale asi dělá i něco míň špatně.