angular-ssr

angular-ssr

热门

使用 @angular/ssr 在 Angular v20+ 中实现服务器端渲染和水合。用于 SSR 设置、水合策略、预渲染静态页面以及处理仅浏览器 API。触发条件包括 SSR 配置、修复水合不匹配、预渲染路由或使代码兼容 SSR。

596Star
70Fork
更新于 2026/3/23
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