Miroslav Holec
Premium

Blazor v .NET 8 je univerzální, ale záludný

Miroslav Holec   13. srpna 2024

Článek se vztahuje k verzi produktu .NET 8

Blazor doznal ve verzi .NET 8 zásadních změn, díky kterým se stal velmi univerzální technologií pro stavbu webových aplikací. V tomto článku bych chtěl nastínit několik záludností, na které se doporučuji při studiu Blazoru zaměřit.

Jednoduchý život s Blazor SSR

Do .NET 8 byl přidán model Blazor SSR. Věnuji mu celý první den mého školení a je to skvělý způsob, jak s Blazorem začít. Blazor SSR využívá celý komponentní model a renderování v tomto případě probíhá kompletně na straně serveru. Zjednodušeně se dá říct, že Blazor SSR funguje podobně (ne stejně) jako prerendering. Protože se jedná o serverovou technologii, může vývojář používat vše, na co byl zvyklý v MVC nebo Razor Pages. Dá se například spolehnout na přítomnost HttpContext třídy a s databází lze pracovat tradičním způsobem (například skrze scoped DbContext injectnutý kam je potřeba). Je to možné i díky tomu, že po každém HTTP požadavku se komponenty důsledně disposují.

I Blazor SSR ale přináší některé výzvy. V první řadě je to enhanced mód. Ten funguje bez přičinění vývojáře. Klikání na webové stránce odchytává blazor.js, který posílá požadavky na server s stará se o automatický update UI. V případě formulářů je tuto funkci nutné zapnout a zde se občas vývojáři potkají s řadou potíží. Zvláštní a logické například je, že při odeslání formuláře se znovu provádí metoda OnInitialized(Async) ještě před tím, než se provede handlovací metoda formuláře. Musí se tedy kontrolovat HTTP metoda a dle toho kód v OnInitialized(Async) přizpůsobit. Protože v enhanced módu provádí aktualizaci DOMu blazor.js, je nutné mu předávat i některé instrukce skrze HTTP hlavičky. Příkladem budiž redirect.

Druhá záludnost je StreamRendering. Když vývojář implementuje asynchronní metodu OnInitializedAsync, tak Blazor provádí obvykle dvojí renderování. První rendering probíhá před prvním awaitováním metody vracející Task a druhé pak na konci metody OnInitializedAsync. Děje se to vždy a vznikají tu chyby. Při prvním renderování musí být totiž UI v renderovatelném stavu. Nelze například provést foreach nad NULL kolekcí. Libovolně lze pak do UI přidat atribut [StreamRendering], který tyto dvě verze UI (čili HTML) dokáže na klienta distribuovat. Bez ohledu na to, zda tuto možnost vývojář použije, renderování vždy probíhá vícekrát (za předpokladu alespoň jednoho awaitování).

A poslední záludnost je v interaktivitě. Pakliže je celá aplikace stavěná v SSR, není se čeho obávat. Když jsou ale části aplikace renderované v SSR a jiné části v režimu interaktivity, pak už je nezbytné programovat univerzálně. Platí totiž, že komponenta - potomek dědí render mode komponenty - rodič. Nelze vynutit jiný render mód. Když vytvořím komponentu LoginForm, musím zkrátka počítat s tím, že ji lze použít v SSR i například ve WASM interactive módu. V prvním odstavci zmíněný HttpContext tak nemusí být vůbec k dispozici.

Záludnosti pokračují v režimu Interactive

Interaktivní režimy reagují na události na straně klienta. Hned tady je první zádrhel, protože SignalR interaktivita sice reaguje na události UI, ale je vykonána na straně serveru. Microsoft přidal do .NET 8 navíc možnost režimu InteractiveAuto, ve kterém si Blazor sám vybírá mezi SignalR a WASM dle aktuální situace. To je skvělá funkce, ale vyžaduje opět tvorbu univerzálního kódu. Není například možné pracovat přímo s databází, protože se zkompilovaný kód přenáší i na klienta. Buď se tedy kód napíše dvakrát (optimálně pro SignalR a optimálně pro WASM) a nebo univerzálně. V obou případech to znamená vytvořit REST API pro poskytování dat v režimu WASM.

A pak tu je prerendering. Prerendering umožňuje na straně serveru vytvořit výchozí kód, který se přenese na klienta. Poté se inicializuje na straně klienta samotná aplikace a dojde k finálnímu renderování v režimu interaktivity. Znamená to, že i v případě InteractiveWebAssembly je při zapnutém prerenderingu nutné napsat kód i pro server. Vývojář by s klidným srdem mohl konstatovat, že "komunikace s REST API funguje na obou stranách". Je to pravda. A pro někoho dost možná akceptovatelná daň.

Jenomže dvojí renderování dat znamená zbytečnou zátěž. I kdyby ne, máme tu jinou výzvu. Když se uživatel přihlásí v režimu InteractiveServer nebo SSR ke vzdálenému API (získá token), je tento token uchován na serveru. Jak zajistit, aby takový token měl k dispozici i klient? Nechceme, aby se uživatel přihlašoval vícekrát. Jak tedy sdílet stav mezi serverem a klientem? Máme tu další kouzlo .NETu, které najdeme pod označením PersistComponentState. Tato třída umí serializovat stav do JSONu, který si lze při inicializaci komponenty vyzvednout. Server si tak může do HTML odložit stav, který se na klientu zparsuje zpět do objektu. Elegantní.

Životní cyklus komponent

Všem popsaným scénářům sekunduje životní cyklus komponent. Ten se totiž v různých situacích mění a jeho pochopení je klíčové při tvorbě Blazor aplikací, které kombinují různé režimy renderování. V režimu SSR a prerenderingu se například neprovádí metoda OnAfterRender(Async), která typicky slouží k JS interoperabilitě při provádění na straně klienta. DOM je ve stavu ready. V praxi to ale neznamená, že by nebylo možné sáhnout do session nebo cookies v OnInitialized(Async). Jenomže takový kód vždy selže v prerenderingu. A infomaci v jakém režimu se komponenta renderuje v .NET 8 nemáme.

Tyto drobné nuance vylučují univerzální rady. Nelze univerzálně říct, že nejlepší metoda pro načtení dat je OnInitialized(Async), OnParametersSet(Async) nebo OnAfterRender(Async). Dokonce v jedné aplikaci musí vývojář v různých situacích volit různé možnosti. Jsou tu navíc hotové komponenty třetích stran, které fungují specifickým způsobem, který je nutné respektovat. Renderování vlastní tabulky v bootstrapu tak bude programováno jinak, než renderování gridu z některého balíku komponent. I uchování stavu do local storage přináší rozhodování. Máme tu sice Protected Browser Storage, který za nás dělá JS interops a ještě jako bonus šifruje data, ovšem funguje jen v režimu InteractiveServer. Oblíbené komponenty Blazored pak sice fungují univerzálně, ale šifrování v nich dostupné není. Čemu dát přednost?

A co takový výsledek zpracování formuláře? V SSR se nabízí napsat si mechanismus ve stylu TempData, jenomže s tím ve WASM nepochodíme. Můžeme tedy vyhodit toast notifikaci, jenomže ta zase nebude fungovat tam, kde není interaktivita.

Zní do děsivě, ale...

... ve skutečnosti stačí pochopit fundamentální principy renderování komponent a životního cyklu. Není potřeba si pamatovat každý podivný use case, který jsem v článku popsal. Ve skutečnosti jsou všechny scénáře jen důsledkem toho, v jakém režimu komponenty renderujeme a co chceme dělat. V životě to pak bývá ještě jednodušší. Vystačit si lze s kombinacemi serverové interaktivity (odpadne vývoj REST API) a nebo vypneme prerendering (a rázem je vývoj také snazší). V mém dvoudenním školení se zaměřuji jak na základní práci s Blazorem v režimu SSR, tak na výše popsané záludnosti režimů interaktivity.