angular-ssr

安装量: 1.8K
排名: #904

安装

npx skills add https://github.com/analogjs/angular-skills --skill angular-ssr

Angular SSR

Implement server-side rendering, hydration, and prerendering in Angular v20+.

Setup Add SSR to Existing Project ng add @angular/ssr

This adds:

@angular/ssr package server.ts - Express server src/main.server.ts - Server bootstrap src/app/app.config.server.ts - Server providers Updates angular.json with SSR configuration Project Structure src/ ├── app/ │ ├── app.config.ts # Browser config │ ├── app.config.server.ts # Server config │ └── app.routes.ts ├── main.ts # Browser bootstrap ├── main.server.ts # Server bootstrap server.ts # Express server

Configuration app.config.server.ts import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; import { provideServerRendering } from '@angular/platform-server'; import { provideServerRoutesConfig } from '@angular/ssr'; import { appConfig } from './app.config'; import { serverRoutes } from './app.routes.server';

const serverConfig: ApplicationConfig = { providers: [ provideServerRendering(), provideServerRoutesConfig(serverRoutes), ], };

export const config = mergeApplicationConfig(appConfig, serverConfig);

Server Routes Configuration // app.routes.server.ts import { RenderMode, ServerRoute } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [ { path: '', renderMode: RenderMode.Prerender, // Static at build time }, { path: 'products', renderMode: RenderMode.Prerender, }, { path: 'products/:id', renderMode: RenderMode.Server, // Dynamic SSR }, { path: 'dashboard', renderMode: RenderMode.Client, // Client-only (SPA) }, { path: '**', renderMode: RenderMode.Server, }, ];

Render Modes Mode Description Use Case RenderMode.Prerender Static HTML at build time Marketing pages, blogs RenderMode.Server Dynamic SSR per request User-specific content RenderMode.Client Client-side only (SPA) Authenticated dashboards Hydration Default Hydration

Hydration is enabled by default with provideClientHydration():

// app.config.ts import { provideClientHydration } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = { providers: [ provideClientHydration(), // ... ], };

Incremental Hydration

Defer hydration of specific components:

@Component({ template: ` @defer (hydrate on viewport) { } @placeholder {

Loading comments...
}

<!-- Hydrate on interaction -->
@defer (hydrate on interaction) {
  <app-interactive-chart [data]="chartData" />
}

<!-- Hydrate on idle -->
@defer (hydrate on idle) {
  <app-recommendations />
}

<!-- Never hydrate (static only) -->
@defer (hydrate never) {
  <app-static-footer />
}

`, }) export class PostComponent { postId = input.required(); chartData = input.required(); }

Hydration Triggers Trigger Description hydrate on viewport When element enters viewport hydrate on interaction On click, focus, or input hydrate on idle When browser is idle hydrate on immediate Immediately after load hydrate on timer(ms) After specified delay hydrate when condition When expression is true hydrate never Never hydrate (static) Event Replay

Capture user events before hydration completes:

import { provideClientHydration, withEventReplay } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = { providers: [ provideClientHydration(withEventReplay()), ], };

Browser-Only Code Platform Detection import { PLATFORM_ID, inject } from '@angular/core'; import { isPlatformBrowser, isPlatformServer } from '@angular/common';

@Component({...}) export class MyComponent { private platformId = inject(PLATFORM_ID);

ngOnInit() { if (isPlatformBrowser(this.platformId)) { // Browser-only code window.addEventListener('scroll', this.onScroll); } } }

afterNextRender / afterRender

Run code only in browser after rendering:

import { afterNextRender, afterRender } from '@angular/core';

@Component({...}) export class ChartComponent { constructor() { // Runs once after first render (browser only) afterNextRender(() => { this.initChart(); });

// Runs after every render (browser only)
afterRender(() => {
  this.updateChart();
});

}

private initChart() { // Safe to use DOM APIs here const canvas = document.getElementById('chart'); new Chart(canvas, this.config); } }

Inject Browser APIs Safely // tokens.ts import { InjectionToken, PLATFORM_ID, inject } from '@angular/core'; import { isPlatformBrowser } from '@angular/common';

export const WINDOW = new InjectionToken('Window', { providedIn: 'root', factory: () => { const platformId = inject(PLATFORM_ID); return isPlatformBrowser(platformId) ? window : null; }, });

export const LOCAL_STORAGE = new InjectionToken('LocalStorage', { providedIn: 'root', factory: () => { const platformId = inject(PLATFORM_ID); return isPlatformBrowser(platformId) ? localStorage : null; }, });

// Usage @Injectable({ providedIn: 'root' }) export class StorageService { private storage = inject(LOCAL_STORAGE);

get(key: string): string | null { return this.storage?.getItem(key) ?? null; }

set(key: string, value: string): void { this.storage?.setItem(key, value); } }

Prerendering Static Routes // app.routes.server.ts export const serverRoutes: ServerRoute[] = [ { path: '', renderMode: RenderMode.Prerender }, { path: 'about', renderMode: RenderMode.Prerender }, { path: 'contact', renderMode: RenderMode.Prerender }, { path: 'blog', renderMode: RenderMode.Prerender }, ];

Dynamic Routes with getPrerenderParams // app.routes.server.ts import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';

export const serverRoutes: ServerRoute[] = [ { path: 'products/:id', renderMode: RenderMode.Prerender, async getPrerenderParams() { // Fetch product IDs to prerender const response = await fetch('https://api.example.com/products'); const products = await response.json(); return products.map((p: Product) => ({ id: p.id })); }, fallback: PrerenderFallback.Server, // SSR for non-prerendered }, { path: 'blog/:slug', renderMode: RenderMode.Prerender, async getPrerenderParams() { const posts = await fetchBlogPosts(); return posts.map(post => ({ slug: post.slug })); }, fallback: PrerenderFallback.Client, // SPA for non-prerendered }, ];

Prerender Fallback Options Fallback Description PrerenderFallback.Server SSR for non-prerendered routes PrerenderFallback.Client Client-side rendering PrerenderFallback.None 404 for non-prerendered routes HTTP Caching TransferState

Automatically transfer HTTP responses from server to client:

import { provideClientHydration, withHttpTransferCacheOptions } from '@angular/platform-browser';

export const appConfig: ApplicationConfig = { providers: [ provideClientHydration( withHttpTransferCacheOptions({ includePostRequests: true, includeRequestsWithAuthHeaders: false, filter: (req) => !req.url.includes('/api/realtime'), }) ), ], };

Manual TransferState import { TransferState, makeStateKey } from '@angular/core';

const PRODUCTS_KEY = makeStateKey('products');

@Injectable({ providedIn: 'root' }) export class ProductService { private http = inject(HttpClient); private transferState = inject(TransferState); private platformId = inject(PLATFORM_ID);

getProducts(): Observable { // Check if data was transferred from server if (this.transferState.hasKey(PRODUCTS_KEY)) { const products = this.transferState.get(PRODUCTS_KEY, []); this.transferState.remove(PRODUCTS_KEY); return of(products); }

return this.http.get<Product[]>('/api/products').pipe(
  tap(products => {
    // Store for transfer on server
    if (isPlatformServer(this.platformId)) {
      this.transferState.set(PRODUCTS_KEY, products);
    }
  })
);

} }

Build and Deploy Build Commands

Build with SSR

ng build

Output structure

dist/ ├── my-app/ │ ├── browser/ # Client assets │ └── server/ # Server bundle

Run SSR Server

Development

npm run serve:ssr:my-app

Production

node dist/my-app/server/server.mjs

Deploy to Node.js Host // server.ts (generated) import { APP_BASE_HREF } from '@angular/common'; import { CommonEngine } from '@angular/ssr/node'; import express from 'express'; import { dirname, join, resolve } from 'node:path'; import { fileURLToPath } from 'node:url'; import bootstrap from './src/main.server';

const serverDistFolder = dirname(fileURLToPath(import.meta.url)); const browserDistFolder = resolve(serverDistFolder, '../browser'); const indexHtml = join(serverDistFolder, 'index.server.html');

const app = express(); const commonEngine = new CommonEngine();

app.get('*', express.static(browserDistFolder, { maxAge: '1y', index: false }));

app.get('*', (req, res, next) => { commonEngine .render({ bootstrap, documentFilePath: indexHtml, url: req.originalUrl, publicPath: browserDistFolder, providers: [{ provide: APP_BASE_HREF, useValue: req.baseUrl }], }) .then((html) => res.send(html)) .catch((err) => next(err)); });

app.listen(4000, () => { console.log('Server listening on http://localhost:4000'); });

For advanced patterns, see references/ssr-patterns.md.

返回排行榜