You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何在服务端而非客户端渲染Firestore数据?

服务端渲染数据失败问题排查

问题详情

  • 目标:实现服务端渲染数据
  • 预期结果:数据由服务端完成渲染
  • 实际结果:数据仅在客户端加载渲染
  • 采用架构模式:Component -> call Use Case -> call Data Source

相关代码实现

Component

@Component({
  selector: 'app-experience',
  templateUrl: './experience.component.html',
  styleUrls: ['./experience.component.scss']
})
export class ExperienceComponent implements OnInit {
  isBrowser: boolean;

  slug: string | null;

  experience!: ExperienceEntity | null;

  subscriptions: Subscription = new Subscription();

  constructor(
    @Inject(PLATFORM_ID) platformID: string,
    private route: ActivatedRoute,
    private getExperienceBySlugUseCase: GetExperienceBySlugUseCase,
  ) {
    this.isBrowser = isPlatformBrowser(platformID);
    this.slug = this.route.snapshot.paramMap.get('experience');

    this.getExperienceBySlug();
  }

  ngOnInit(): void {
  }

  getExperienceBySlug() {
    if (this.slug == null) { return }

    let subscription = this.getExperienceBySlugUseCase
      .get(this.slug)
      .subscribe(value => this.experience = value)

    this.subscriptions.add(subscription)
  }

  ngOnDestroy(): void {
    this.subscriptions.unsubscribe();
  }
}

Use Case

@Injectable()
export class GetExperienceBySlugUseCase {

  constructor(private getExperienceBySlugDataSource: GetExperienceBySlugDataSource) { }

  get(slug: string): Observable<ExperienceEntity | null> {
    const aObservable = this.getExperienceBySlugDataSource.get(slug);

    return aObservable.pipe(
      map(val => {
        if (val.length == 0) { return null }

        return {
          title: val[0]['experienceTitle'],
          type: val[0]['experienceType'],
          category: val[0]['experienceCategory'],
          location: {
            city: val[0]['location']['city'],
            country: val[0]['location']['country'],
          },
          coverPhoto: {
            url: val[0]['coverPhoto']
          }
        } as ExperienceEntity
      })
    )
  }
}

Data Source

@Injectable()
export class GetExperienceBySlugDataSource {
  firestore: Firestore = inject(Firestore);

  constructor() { }

  get(slug: string) {
    const aQuery = query(
      collection(this.firestore, "experiences"),
      where("slug", "==", slug)
    )

    return collectionData(aQuery);
  }
}

问题原因

当前代码在组件构造函数中发起异步请求,但Angular SSR不会等待构造函数里的异步操作完成就生成HTML返回给客户端,导致服务端渲染的页面没有数据,只能等客户端加载后再请求数据渲染。另外,Firestore的collectionData在服务端环境下可能未正确初始化,无法获取数据。

解决方案

1. 用路由Resolver预取数据

通过路由解析器在路由激活前加载数据,服务端会等待数据加载完成后再渲染组件。

新建Resolver

@Injectable({ providedIn: 'root' })
export class ExperienceResolver implements Resolve<ExperienceEntity | null> {
  constructor(private getExperienceBySlugUseCase: GetExperienceBySlugUseCase) {}

  resolve(route: ActivatedRouteSnapshot): Observable<ExperienceEntity | null> {
    const slug = route.paramMap.get('experience');
    return slug ? this.getExperienceBySlugUseCase.get(slug) : of(null);
  }
}

修改路由配置

const routes: Routes = [
  {
    path: 'experience/:experience',
    component: ExperienceComponent,
    resolve: {
      experience: ExperienceResolver
    }
  }
];

更新Component

@Component({
  selector: 'app-experience',
  templateUrl: './experience.component.html',
  styleUrls: ['./experience.component.scss']
})
export class ExperienceComponent implements OnInit {
  experience!: ExperienceEntity | null;

  constructor(private route: ActivatedRoute) {}

  ngOnInit(): void {
    // 直接从路由解析结果中获取数据
    this.experience = this.route.snapshot.data['experience'];
  }
}

2. 适配Firestore服务端环境

确保Angular SSR中Firestore能正常工作,在app.server.module.ts中正确配置Firebase:

import { provideFirebaseApp, initializeApp } from '@angular/fire/app';
import { provideFirestore, getFirestore } from '@angular/fire/firestore';
import { environment } from '../environments/environment';

@NgModule({
  imports: [
    provideFirebaseApp(() => initializeApp(environment.firebase)),
    provideFirestore(() => getFirestore())
  ]
})
export class AppServerModule {}

3. 用TransferState缓存服务端数据

避免客户端重复请求,同时确保服务端渲染的数据能在客户端复用:

import { TransferState, makeStateKey } from '@angular/platform-browser';
import { PLATFORM_ID, inject } from '@angular/core';
import { isPlatformServer } from '@angular/common';
import { of } from 'rxjs';
import { take } from 'rxjs/operators';

const EXPERIENCE_STATE_KEY = makeStateKey<ExperienceEntity[] | null>('experience');

@Injectable()
export class GetExperienceBySlugDataSource {
  firestore: Firestore = inject(Firestore);
  private transferState = inject(TransferState);
  private platformId = inject(PLATFORM_ID);

  constructor() { }

  get(slug: string) {
    // 先检查是否有缓存数据
    const cachedData = this.transferState.get(EXPERIENCE_STATE_KEY, null);
    if (cachedData) {
      return of(cachedData);
    }

    const aQuery = query(
      collection(this.firestore, "experiences"),
      where("slug", "==", slug)
    );

    const data$ = collectionData(aQuery);
    
    // 服务端环境下,将数据存入TransferState
    if (isPlatformServer(this.platformId)) {
      data$.pipe(take(1)).subscribe(data => {
        this.transferState.set(EXPERIENCE_STATE_KEY, data);
      });
    }

    return data$;
  }
}

总结

通过路由Resolver预取数据,确保服务端等待数据加载完成后再渲染页面,同时适配Firestore的服务端环境并使用TransferState缓存数据,就能实现数据的服务端渲染。

内容的提问来源于stack exchange,提问作者Jason Rich Darmawan

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.10 05:47:10