SKILL.md
readonly只读
name
angular-ssr
description
使用 @angular/ssr 在 Angular v20+ 中实现服务器端渲染和水合。用于 SSR 设置、水合策略、预渲染静态页面以及处理仅浏览器 API。触发条件包括 SSR 配置、修复水合不匹配、预渲染路由或使代码兼容 SSR。
Angular SSR
在 Angular v20+ 中实现服务器端渲染、水合和预渲染。
设置
为现有项目添加 SSR
ng add @angular/ssr
这将添加:
@angular/ssr包server.ts- Express 服务器src/main.server.ts- 服务器引导文件src/app/app.config.server.ts- 服务器提供者- 更新
angular.json中的 SSR 配置
项目结构
src/
├── app/
│ ├── app.config.ts # 浏览器配置
│ ├── app.config.server.ts # 服务器配置
│ └── app.routes.ts
├── main.ts # 浏览器引导文件
├── main.server.ts # 服务器引导文件
server.ts # Express 服务器
配置
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);
服务器路由配置
// 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,
},
];
渲染模式
| 模式 | 描述 | 使用场景 |
|---|---|---|
RenderMode.Prerender |
构建时生成静态 HTML | 营销页面、博客 |
RenderMode.Server |
每次请求动态 SSR | 用户特定内容 |
RenderMode.Client |
仅客户端渲染(SPA) | 需要认证的仪表盘 |
水合
默认水合
默认通过 provideClientHydration() 启用水合:
// app.config.ts
import { provideClientHydration } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(),
// ...
],
};
增量水合
延迟特定组件的水合:
@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>();
}
水合触发器
| 触发器 | 描述 |
|---|---|
hydrate on viewport |
当元素进入视口时 |
hydrate on interaction |
点击、聚焦或输入时 |
hydrate on idle |
浏览器空闲时 |
hydrate on immediate |
加载后立即 |
hydrate on timer(ms) |
指定延迟后 |
hydrate when condition |
表达式为真时 |
hydrate never |
永不水合(静态) |
事件重放
在水合完成前捕获用户事件:
import { provideClientHydration, withEventReplay } from '@angular/platform-browser';
export const appConfig: ApplicationConfig = {
providers: [
provideClientHydration(withEventReplay()),
],
};
仅浏览器代码
平台检测
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
仅在浏览器渲染后运行代码:
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);
}
}
安全注入浏览器 API
// 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);
}
}
预渲染
静态路由
// 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 },
];
带 getPrerenderParams 的动态路由
// 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
},
];
预渲染回退选项
| 回退 | 描述 |
|---|---|
PrerenderFallback.Server |
未预渲染的路由使用 SSR |
PrerenderFallback.Client |
客户端渲染 |
PrerenderFallback.None |
未预渲染的路由返回 404 |
HTTP 缓存
TransferState
自动将 HTTP 响应从服务器传输到客户端:
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'),
})
),
],
};
手动 TransferState
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);
}
})
);
}
}
构建与部署
构建命令
# 构建 SSR
ng build
# 输出结构
dist/
├── my-app/
│ ├── browser/ # 客户端资源
│ └── server/ # 服务器包
运行 SSR 服务器
# 开发
npm run serve:ssr:my-app
# 生产
node dist/my-app/server/server.mjs
部署到 Node.js 主机
// 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('服务器正在监听 http://localhost:4000');
});
高级模式请参见 references/ssr-patterns.md。






