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

Angular 18 SSR渲染异常:骨架屏与内容同时显示问题求助

问题分析与解决方案

核心问题原因

  1. 服务端未正确传递加载状态:你在getProductsPaginated的map操作中硬编码了loading: false,完全忽略了Apollo watchQuery本身返回的loading状态。这会导致Signal无法正确反映数据的真实加载状态,初始的initialValue: { loading: true }和Observable发射的第一个值(loading: false)之间的快速切换,加上SSR的渲染机制,就会出现骨架屏和卡片短暂共存的情况。

  2. 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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.21 13:37:03