Angular 18 SSR渲染异常:骨架屏与内容同时显示问题求助
问题分析与解决方案
核心问题原因
服务端未正确传递加载状态:你在
getProductsPaginated的map操作中硬编码了loading: false,完全忽略了ApollowatchQuery本身返回的loading状态。这会导致Signal无法正确反映数据的真实加载状态,初始的initialValue: { loading: true }和Observable发射的第一个值(loading: false)之间的快速切换,加上SSR的渲染机制,就会出现骨架屏和卡片短暂共存的情况。SSR客户端-服务端状态不匹配:服务器端渲染时已经完成了数据请求,生成了商品卡片的HTML,但客户端初始化时Signal使用了
loading: true的初始值,导致客户端会先渲染骨架屏,随后在hydration完成后切换为商品卡片,这就造成了两者同时显示的视觉问题。
解决方案
步骤1:修复服务中的加载状态传递
修改product.service.ts,正确传递Apollo返回的loading状态,同时处理数据可能为空的边界情况:
@Injectable({ providedIn: 'root' }) export class ProductsService { private apollo = inject(Apollo); getProductsPaginated(quantity:number, page:number): Observable<ProductResponse>{ return this.apollo.watchQuery({ query: GET_PRODUCTS, variables: { quantity, page } }).valueChanges.pipe( map(({ data, loading, error }: any) => ({ loading, // 直接使用Apollo返回的loading状态 products: data?.productsPaginated?.nodes || [], // 处理data为空的情况 error: error })) ); } }
步骤2:使用TransferState同步服务端数据到客户端
为了避免客户端重新请求数据,确保客户端初始状态与服务端一致,需要用TransferState传递服务端获取的数据:
首先在shop.component.ts中引入并使用TransferState:
import { TransferState, makeStateKey } from '@angular/platform-browser'; import { inject } from '@angular/core'; import { of } from 'rxjs'; const PRODUCTS_STATE_KEY = makeStateKey<ProductResponse>('products_paginated'); @Component({ selector: 'app-shop', standalone: true, imports: [SidebarMenuComponent, ProductCardComponent, ProductCardSkeletonComponent], templateUrl: './shop.component.html', styleUrl: './shop.component.css' }) export default class ShopComponent{ public skeletonProductsLenght = [1, 2, 3, 4, 5, 6]; private productsService = inject(ProductsService); private transferState = inject(TransferState); public productsPaginatedResp = toSignal( // 优先从TransferState取数据,没有再发起请求 this.transferState.hasKey(PRODUCTS_STATE_KEY) ? of(this.transferState.get(PRODUCTS_STATE_KEY, { loading: false, products: [], error: false })) : this.productsService.getProductsPaginated(12,1), { initialValue: this.transferState.get(PRODUCTS_STATE_KEY, { loading: true, products: [], error: false }) } ); constructor() { // 服务端渲染时,将获取的数据存入TransferState if (typeof window === 'undefined') { this.productsService.getProductsPaginated(12,1).subscribe(resp => { this.transferState.set(PRODUCTS_STATE_KEY, resp); }); } } }
步骤3:优化模板逻辑(可选)
可以简化模板中的条件判断,避免重复调用productsPaginatedResp():
<div class="grid md:grid-cols-3 grid-cols-2 gap-6 p-4"> @let resp = productsPaginatedResp(); @if(resp.loading){ @for(item of skeletonProductsLenght; track item){ <app-product-card-skeleton/> } } @else { @for (item of resp.products; track item.key) { <app-product-card [product]="item"/> } } </div>
说明
- 修复服务的loading状态后,Signal能准确反映数据加载的真实状态,避免错误的状态切换。
- 使用TransferState后,客户端会直接复用服务端已获取的数据,不会出现初始loading状态与服务端渲染内容不匹配的问题,彻底解决骨架屏和卡片同时显示的情况。
内容的提问来源于stack exchange,提问作者Dr oscar
相关产品推荐
相关产品推荐

