02 Text článku
Angular umí renderovat stránku na třech místech a od Angularu 19 umí pro každou routu zvlášť rozhodnout, na kterém. Tomu se říká hybrid rendering. Samotnou tabulku rout se naučíte za deset minut. Déle trvá zjistit, co se kolem stránky změní, jakmile je prerenderovaná: její data se přestanou aktualizovat, její request zmizí a hosting ji začne servírovat dřív, než se k ní dostane váš server. Tento článek pokrývá obě poloviny, nejdřív konstrukce a potom důsledky.
Tři místa pro rendering stránky
Na klientovi server odpoví prázdným shellem, prohlížeč stáhne bundle a postaví stránku od nuly. Tak byla single-page aplikace zamýšlená a pořád je to správná volba pro všechno, co dává smysl až ve chvíli, kdy víte, kdo je uživatel.
Na serveru se stránka při každém requestu vyrenderuje do HTML dřív, než odejde. Návštěvník i crawler dostanou obsah hned v prvním bajtu a server může dělat to, co prohlížeč dělat nemá: držet tajemství, sáhnout do databáze, cachovat výsledek pro všechny.
Při buildu se stránka vyrenderuje jednou, v CI, a výstupem je soubor. Žádný server už pro ni nikdy nepoběží. Tenhle režim se vrátí nejrychleji, protože na většině webů se překvapivě velká část stránek mění, jen když někdo deployuje, a renderovat je na vyžádání znamená platit za stejnou práci při každém cache missu.
Hybrid rendering znamená vybrat jedno z těch tří pro každou routu, ne pro celou aplikaci.
Dokumentace: https://angular.dev/guide/hybrid-rendering
Tabulka rout
Serverové routy se deklarují vedle klientských a předávají se do provideServerRendering. V Angularu 19 to byl samostatný provider provideServerRouting(); dnes žije uvnitř provideServerRendering():
bootstrapApplication(AppComponent, {
providers: [provideServerRendering(withRoutes(SERVER_ROUTES))],
});
Každý záznam určuje cestu a renderMode:
export const SERVER_ROUTES: ServerRoute[] = [
{ path: 'pricing', renderMode: RenderMode.Prerender },
{ path: 'contact', renderMode: RenderMode.Prerender },
{ path: 'blog/:slug', renderMode: RenderMode.Server },
{ path: 'account/**', renderMode: RenderMode.Client },
{ path: '**', renderMode: RenderMode.Server },
];
Ceník a kontakt se mění s deployem, takže z nich budou soubory. Článek publikuje editor bez deploye, takže se renderuje pro každý request a cachuje se. Sekce účtu potřebuje uživatele, takže server pošle shell a převezme to klient. Všechno ostatní ve výchozím stavu připadne serveru.
Tři pravidla, která za vás tabulka nevynutí:
Wildcard záznam je povinný. Routa, které neodpovídá žádný záznam, má výchozí režim
Prerendera engine odmítne prerenderovat až při requestu, takže bez záznamu**neznámá URL selže, místo aby došla na vaši stránku 404.Rozhoduje specifičnost, ne pořadí v poli. Přesné cesty vyhrají, ať stojí kdekoli; catch-all je na konci jen kvůli čitelnosti.
Lokalizovaná cesta je samostatná routa. Pokud je jazyk segmentem cesty (
/en/pricing,/cs/pricing), každý jazyk potřebuje vlastní záznam. Pomocná funkce, která stránku namapuje na všechny podporované jazyky, udrží tabulku poctivou:
const localised = (page: string): ServerRoute[] =>
SUPPORTED_LANGUAGES.map(lang => ({
path: `${lang}/${page}`,
renderMode: RenderMode.Prerender,
}));
export const SERVER_ROUTES: ServerRoute[] = [
...PRERENDERED_PAGES.flatMap(localised),
{ path: '**', renderMode: RenderMode.Server },
];
Užitečné konstrukce
Parametry a fallback
Routu s parametrem jde prerenderovat, pokud build dokáže vyjmenovat jeho hodnoty. getPrerenderParams běží v injection contextu buildu, takže může používat vaše služby:
{
path: 'product/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const products = inject(ProductsService);
const top = await products.topSellers();
return top.map(product => ({ id: product.id }));
},
fallback: PrerenderFallback.Server,
}
fallback určuje, co se stane s id, které build nevyrenderoval. PrerenderFallback.Server ho vyrenderuje na request, Client pošle prázdný shell, None odpoví 404. Kombinace výše je běžný případ pro katalog: nejprodávanější produkty jsou soubory, dlouhý chvost zůstává dynamický. Funguje i catch-all parametr: path: 'docs/**' s parametry jako { '**': 'guide/routing' }.
Stavový kód a hlavičky
Každý záznam může nést status a headers. Pro statický stavový kód je to mnohem jednodušší než sahat na response přes token RESPONSE_INIT:
{ path: 'page-not-found', renderMode: RenderMode.Server, status: 404 },
{ path: 'old-pricing', renderMode: RenderMode.Server, status: 410 },
{
path: 'preview/**',
renderMode: RenderMode.Server,
headers: { 'X-Robots-Tag': 'noindex' },
},
Na prerenderované routě platí jen tehdy, když soubor servíruje Angular engine. Hosting, který ho servíruje jako statický asset, je nikdy neuvidí, a k tomu se ještě vrátíme.
Client mode jako zadní vrátka
RenderMode.Client je pro stránky, které bez uživatele nedávají smysl: dashboard za přihlášením, stránka účtu, editor. Je to také cesta ven pro komponentu, která v Node vůbec nemůže běžet (canvasová knihovna, WebGL graf). Alternativou je nechat routu renderovat na serveru a odložit jen tu jednu komponentu:
@defer (on viewport; hydrate on viewport) {
<app-globe-chart [data]="data()" />
} @placeholder {
<app-chart-skeleton />
}
@defer drží komponentu mimo serverový render a trigger hydrate (incremental hydration, zapnutá přes withIncrementalHydration()) ji drží i mimo bootstrap na klientovi, dokud se neobjeví ve viewportu.
Režim statického výstupu
outputMode: 'server' v nastavení builderu je to, co vygeneruje manifest app enginu pro režimy výše. outputMode: 'static' prerenderuje všechny routy a nedodává žádný server, což je správná volba pro web bez čehokoli dynamického a bez API za ním: dokumentaci, portfolio, launch stránku.
Co se rozbije, jakmile je stránka prerenderovaná
Všechno výše je tabulka rout. Zbytek článku je o tom, co tabulka rout udělá se stránkou, a právě tam se skrývá většina produkčních překvapení.
Data jsou zmrazená, a ne jen do hydratace
Prerender spustí načítání dat stránky během buildu, s tím, co API v tu chvíli odpoví, a výsledek zůstane zmrazený až do dalšího buildu. To se dá čekat. Méně se čeká, že zmrazení neskončí, když převezme řízení klient.
Z toho plyne pravidlo, které stránky se hodí: prerenderujte stránku, když se její obsah mění jen s deployem.
Pokud stránka musí být prerenderovaná a po načtení přesto ukazovat čerstvá data, vyřaďte její requesty z transfer cache, místo abyste bojovali s politikou cache:
provideClientHydration(
withEventReplay(),
withHttpTransferCacheOptions({
filter: req => !req.url.includes('/api/stock'),
}),
),
Vyřazený request se na klientovi spustí znovu a stránka za něj zaplatí jedním round tripem.
Žádný request není
Obvyklou obětí je kanonická URL. Číst ji z REQUEST.url funguje pro každý request; při prerenderu je null a fallback na holý origin potichu prohlásí každou prerenderovanou stránku za kopii homepage. PlatformLocation se inicializuje z URL renderu v obou případech, takže právě to je fallback, který funguje:
private renderUrl(): { pathname: string; search: string } {
return this.request ? new URL(this.request.url) : this.platformLocation;
}
Kontrolujte build
Build proto potřebuje vlastní kontrolu, která běží po prerenderu. Skript, který projde každý prerenderovaný soubor, ne jen vzorek:
soubor pro každou očekávanou routu existuje a je jich přesně tolik, kolik předpovídá tabulka rout;
HTML neobsahuje žádnou ze značek, které znamenají, že render selhal (selektor elementu chybové komponenty, stránky 404, prázdný
<router-outlet>);transfer state je přítomný a nese data, která si stránka načítá sama.
Poslední kontrola má být pro každou stránku zvlášť, ne plošná. Layout, který spouští dotaz na každé routě (menu, patička, pruh souvisejícího obsahu), naplní cache na každé stránce, takže „cache není prázdná“ projde, i když vlastní sekce stránky vyrenderovala prázdno. Pojmenujte dotaz, na který musí mít každá stránka odpověď, a povolte ty, které smějí být legitimně prázdné (stránka kariéry bez otevřené pozice).
Kontrola by si měla stěžovat i na zastaralý snapshot: routa odebraná z tabulky nechá svůj soubor v dist, když se výstupní cesta nečistí, a hosting ho dál servíruje.
Hosting servíruje soubor dřív, než se spustí váš server
Většina hostingů odpovídá ze statického výstupu dřív, než vůbec zváží rewrite na funkci: Firebase Hosting to tak dělá, stejně jako Cloudflare a Vercel. Prerenderovaná stránka je tam soubor, takže pro tu URL ve vašem Express serveru nepoběží nic. Ani middleware pro bezpečnostní hlavičky, ani redirect na kanonický host, ani logika cache-control, ani žádná zadní vrátka typu ?nocache, která jste si postavili, a ani headers z tabulky rout.
Hlavičky padnou jako první. Pokud jsou hlavičky dokumentu (X-Frame-Options, CSP, Referrer-Policy, Permissions-Policy) Express middleware, prerenderované stránky odejdou bez nich. Musí být deklarované i v konfiguraci hostingu a obě kopie pak musí zůstat totožné: hosting aplikuje svá pravidla i na odpověď funkce, takže hlavička nastavená na obou místech s různými hodnotami je konflikt, ne fallback. Držte jeden zdroj: soubor s konstantami, který importuje server, a test, který proti němu porovná konfiguraci hostingu.
{
"source": "**/!(*.*)",
"headers": [
{ "key": "X-Frame-Options", "value": "SAMEORIGIN" },
{ "key": "Referrer-Policy", "value": "strict-origin-when-cross-origin" }
]
}
Váš vlastní statický middleware
Express server pořád servíruje stejné soubory lokálně a na každý request, který dojde až k funkci. Prerenderovaná stránka je na disku adresář (pricing/index.html) a výchozí nastavení serve-static přesměruje /pricing na /pricing/, jakmile ten adresář existuje. Pokud má server navíc redirect na koncové lomítko, ti dva si návštěvníka přehazují tam a zpět. index: false, redirect: false smyčku ukončí a servírování souboru nechá na enginu:
server.use(
express.static(browserDistFolder, {
fallthrough: true,
index: false,
redirect: false,
}),
);
Cachování rout, které zůstávají dynamické
Prerendering přesune ze serveru jen statické stránky. Routy renderované pro každý request pořád potřebují politiku cache a nejlepší je klíčovat ji podle vyrenderovaného statusu: dlouhá životnost ve sdílené cache pro 2xx, krátká pro vyrenderovanou 404 (obvyklým důvodem je slug, který se chystá k publikaci), žádná pro redirect, který závisí na Accept-Language, a žádná pro 5xx, aby to další návštěvník zkusil znovu. Nastavte ji až poté, co engine odpověděl, protože teprve tehdy je status známý.
Mezi oběma režimy leží jeden, který Angular nativně nemá: stránka, která se mění několikrát denně. Nejblíž mu je routa renderovaná pro každý request za CDN s s-maxage a stale-while-revalidate, která stojí jeden render za životnost na každém edge, nebo plánovaný rebuild. Vyberte levnější z obou podle toho, jak často se stránka mění, místo abyste ji cpali do seznamu prerenderovaných.
Kontrolní seznam
Prerenderujte routu, jen když se její obsah mění s deployem. Transfer state zmrazí načtená data na celou session, ne jen na první vykreslení.
Vyjmenujte parametry přes
getPrerenderParamsa zvoltefallbackpro hodnoty, které build neviděl.Jeden záznam na jazyk, režim
Clientpro stránky, které potřebují uživatele,@deferpro tu jednu komponentu, která nemůže běžet v Node, a záznam**v režimuServer.Nic v prerenderovaném renderu nesmí záviset na
REQUESTaniRESPONSE_INIT. Pro URL sáhněte jako fallback poPlatformLocation.Kontrolujte build: projděte každý prerenderovaný soubor, selžte na chybových značkách a chybějících datech, spočítejte soubory.
Při deployi obcházejte build cache.
Zopakujte hlavičky dokumentu v konfiguraci hostingu, napárované na clean URL, bez
Cache-Control.index: false, redirect: falsena vlastním statickém middlewaru.Klíčujte životnost cache pro routy renderované na request podle vyrenderovaného statusu.
