如何在服务端而非客户端渲染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
相关产品推荐
相关产品推荐

