angular-ssr
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseAngular SSR
Angular SSR
Implement server-side rendering, hydration, and prerendering in Angular v20+.
在Angular v20+中实现服务端渲染(SSR)、水合与预渲染。
Setup
配置步骤
Add SSR to Existing Project
为现有项目添加SSR
bash
ng add @angular/ssrThis adds:
- package
@angular/ssr - - Express server
server.ts - - Server bootstrap
src/main.server.ts - - Server providers
src/app/app.config.server.ts - Updates with SSR configuration
angular.json
bash
ng add @angular/ssr此命令会添加:
- 包
@angular/ssr - - Express 服务器
server.ts - - 服务器启动文件
src/main.server.ts - - 服务器提供者配置
src/app/app.config.server.ts - 更新 中的SSR配置
angular.json
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 serversrc/
├── app/
│ ├── app.config.ts # 浏览器端配置
│ ├── app.config.server.ts # 服务器端配置
│ └── app.routes.ts
├── main.ts # 浏览器端启动文件
├── main.server.ts # 服务器端启动文件
server.ts # Express 服务器Configuration
配置详情
app.config.server.ts
app.config.server.ts
typescript
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);typescript
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
服务器路由配置
typescript
// 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,
},
];typescript
// app.routes.server.ts
import { RenderMode, ServerRoute } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: '',
renderMode: RenderMode.Prerender, // 构建时生成静态内容
},
{
path: 'products',
renderMode: RenderMode.Prerender,
},
{
path: 'products/:id',
renderMode: RenderMode.Server, // 动态SSR
},
{
path: 'dashboard',
renderMode: RenderMode.Client, // 仅客户端渲染(SPA)
},
{
path: '**',
renderMode: RenderMode.Server,
},
];Render Modes
渲染模式
| Mode | Description | Use Case |
|---|---|---|
| Static HTML at build time | Marketing pages, blogs |
| Dynamic SSR per request | User-specific content |
| Client-side only (SPA) | Authenticated dashboards |
| 模式 | 描述 | 使用场景 |
|---|---|---|
| 构建时生成静态HTML | 营销页面、博客 |
| 每次请求时动态SSR | 用户专属内容 |
| 仅客户端渲染(SPA) | 需认证的仪表盘 |
Hydration
水合处理
Default Hydration
默认水合
Hydration is enabled by default with :
provideClientHydration()typescript
// app.config.ts
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
// ...
],
};通过默认启用水合:
provideClientHydration()typescript
// app.config.ts
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
// ...
],
};Incremental Hydration
增量水合
Defer hydration of specific components:
typescript
@Component({
template: `
<!-- Hydrate when visible -->
@defer (hydrate on viewport) {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-placeholder">Loading comments...</div>
}
<!-- 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 Post {
postId = input.required<string>();
chartData = input.required<ChartData>();
}延迟特定组件的水合:
typescript
@Component({
template: `
<!-- 进入视口时水合 -->
@defer (hydrate on viewport) {
<app-comments [postId]="postId" />
} @placeholder {
<div class="comments-placeholder">加载评论中...</div>
}
<!-- 交互时水合 -->
@defer (hydrate on interaction) {
<app-interactive-chart [data]="chartData" />
}
<!-- 浏览器空闲时水合 -->
@defer (hydrate on idle) {
<app-recommendations />
}
<!-- 永不水合(仅静态) -->
@defer (hydrate never) {
<app-static-footer />
}
`,
})
export class Post {
postId = input.required<string>();
chartData = input.required<ChartData>();
}Hydration Triggers
水合触发条件
| Trigger | Description |
|---|---|
| When element enters viewport |
| On click, focus, or input |
| When browser is idle |
| Immediately after load |
| After specified delay |
| When expression is true |
| Never hydrate (static) |
| 触发条件 | 描述 |
|---|---|
| 元素进入视口时 |
| 点击、聚焦或输入时 |
| 浏览器空闲时 |
| 加载完成后立即触发 |
| 指定延迟时间后触发 |
| 表达式为真时 |
| 永不水合(静态) |
Event Replay
事件重放
Capture user events before hydration completes:
typescript
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
],
};在水合完成前捕获用户事件:
typescript
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
],
};Browser-Only Code
仅浏览器端代码处理
Platform Detection
平台检测
typescript
import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({...})
export class My {
private platformId = inject(PLATFORM_ID);
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// Browser-only code
window.addEventListener('scroll', this.onScroll);
}
}
}typescript
import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser, isPlatformServer } from '@angular/common';
@Component({...})
export class My {
private platformId = inject(PLATFORM_ID);
ngOnInit() {
if (isPlatformBrowser(this.platformId)) {
// 仅浏览器端执行的代码
window.addEventListener('scroll', this.onScroll);
}
}
}afterNextRender / afterRender
afterNextRender / afterRender
Run code only in browser after rendering:
typescript
import { afterNextRender, afterRender } from '@angular/core';
@Component({...})
export class Chart {
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);
}
}渲染完成后仅在浏览器端执行代码:
typescript
import { afterNextRender, afterRender } from '@angular/core';
@Component({...})
export class Chart {
constructor() {
// 首次渲染完成后执行一次(仅浏览器端)
afterNextRender(() => {
this.initChart();
});
// 每次渲染完成后执行(仅浏览器端)
afterRender(() => {
this.updateChart();
});
}
private initChart() {
// 在此处安全使用DOM API
const canvas = document.getElementById('chart');
new Chart(canvas, this.config);
}
}Inject Browser APIs Safely
安全注入浏览器API
typescript
// tokens.ts
import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export const WINDOW = new InjectionToken<Window | null>('Window', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
return isPlatformBrowser(platformId) ? window : null;
},
});
export const LOCAL_STORAGE = new InjectionToken<Storage | null>('LocalStorage', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
return isPlatformBrowser(platformId) ? localStorage : null;
},
});
// Usage
@Injectable({ providedIn: 'root' })
export class Storage {
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);
}
}typescript
// tokens.ts
import { InjectionToken, PLATFORM_ID, inject } from '@angular/core';
import { isPlatformBrowser } from '@angular/common';
export const WINDOW = new InjectionToken<Window | null>('Window', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
return isPlatformBrowser(platformId) ? window : null;
},
});
export const LOCAL_STORAGE = new InjectionToken<Storage | null>('LocalStorage', {
providedIn: 'root',
factory: () => {
const platformId = inject(PLATFORM_ID);
return isPlatformBrowser(platformId) ? localStorage : null;
},
});
// 使用示例
@Injectable({ providedIn: 'root' })
export class Storage {
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
静态路由
typescript
// 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 },
];typescript
// 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
带getPrerenderParams的动态路由
typescript
// 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
},
];typescript
// app.routes.server.ts
import { RenderMode, ServerRoute, PrerenderFallback } from '@angular/ssr';
export const serverRoutes: ServerRoute[] = [
{
path: 'products/:id',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
// 获取需要预渲染的产品ID
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
},
{
path: 'blog/:slug',
renderMode: RenderMode.Prerender,
async getPrerenderParams() {
const posts = await fetchBlogPosts();
return posts.map(post => ({ slug: post.slug }));
},
fallback: PrerenderFallback.Client, // 未预渲染的路由使用SPA
},
];Prerender Fallback Options
预渲染回退选项
| Fallback | Description |
|---|---|
| SSR for non-prerendered routes |
| Client-side rendering |
| 404 for non-prerendered routes |
| 回退方式 | 描述 |
|---|---|
| 未预渲染的路由使用SSR |
| 未预渲染的路由使用客户端渲染 |
| 未预渲染的路由返回404 |
HTTP Caching
HTTP缓存
TransferState
TransferState
Automatically transfer HTTP responses from server to client:
typescript
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'),
})
),
],
};自动将服务器端的HTTP响应传递到客户端:
typescript
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
手动TransferState
typescript
import { TransferState, makeStateKey } from '@angular/core';
const PRODUCTS_KEY = makeStateKey<Product[]>('products');
@Injectable({ providedIn: 'root' })
export class Product {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
getProducts(): Observable<Product[]> {
// 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);
}
})
);
}
}typescript
import { TransferState, makeStateKey } from '@angular/core';
const PRODUCTS_KEY = makeStateKey<Product[]>('products');
@Injectable({ providedIn: 'root' })
export class Product {
private http = inject(HttpClient);
private transferState = inject(TransferState);
private platformId = inject(PLATFORM_ID);
getProducts(): Observable<Product[]> {
// 检查是否有从服务器传递的数据
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 => {
// 在服务器端存储数据以便传递
if (isPlatformServer(this.platformId)) {
this.transferState.set(PRODUCTS_KEY, products);
}
})
);
}
}Build and Deploy
构建与部署
Build Commands
构建命令
bash
undefinedbash
undefinedBuild with SSR
带SSR的构建
ng build
ng build
Output structure
输出结构
dist/
├── my-app/
│ ├── browser/ # Client assets
│ └── server/ # Server bundle
undefineddist/
├── my-app/
│ ├── browser/ # 客户端资源
│ └── server/ # 服务器端包
undefinedRun SSR Server
启动SSR服务器
bash
undefinedbash
undefinedDevelopment
开发环境
npm run serve:ssr:my-app
npm run serve:ssr:my-app
Production
生产环境
node dist/my-app/server/server.mjs
undefinednode dist/my-app/server/server.mjs
undefinedDeploy to Node.js Host
部署到Node.js主机
javascript
// 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.
javascript
// server.ts(自动生成)
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');
});如需高级模式,请查看 references/ssr-patterns.md。