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

Angular预渲染页面浏览器加载时重复请求数据的原因与解决

问题描述

我有一个简单的路由组件:

@Component({
  ...
})
export class AppLandingComponent implements OnInit {
  
  private contentSignal = signal<MyQuery>(...);

  constructor(
    private apollo: SDKApollo,
    private route: ActivatedRoute,
    private router: Router,
  ) {}

  ngOnInit() {

    const id = this.route.snapshot.data.id;

    this.apollo.runQuery<MyQuery, MyQueryVariables>({
      client: 'cms',
      document: MyDocument,
      variables: {
        name: id
      }
    })
    .subscribe({
      next: (data) => {
        console.log('fetched data');
        this.contentSignal.set(data);
      },
    });
  }
}

我已在prerendered-routes.txt中配置该组件对应的路由为预渲染路由,并在项目配置中指定该文件:

"prerender": {
          "discoverRoutes": false,
          "routesFile": "apps/foo/prerendered-routes.txt"
        },

prerendered-routes.txt内容如下:

/landing/foo
/landing/bar

构建项目后,这两个路由会通过CMS获取内容完成预渲染。启动服务后访问这些路由,网络面板显示已获取预渲染HTML,但浏览器仍会再次请求数据,这违背了预渲染的初衷。若仅在服务端执行数据请求,页面虽能获取预渲染HTML但无法正常加载。请问这是预渲染的特殊机制吗?如何避免重复请求数据?

解决方案

这不是预渲染的特殊机制,核心问题是客户端 hydration(水合)阶段没有复用预渲染时已获取的数据:预渲染仅生成包含静态内容的HTML,但Angular启动客户端应用时,会重新执行组件的初始化逻辑(比如ngOnInit),如果没有数据复用机制,组件会重复发起请求;若只在服务端请求数据,客户端因缺少对应状态,会出现页面无法正常加载的情况。以下是具体解决方法:

1. 利用Apollo客户端的缓存恢复机制

Apollo支持将预渲染阶段的缓存数据序列化到HTML中,客户端初始化时直接读取复用:

预渲染/服务端阶段

执行查询后,将Apollo缓存导出为JSON并注入到HTML模板:

// 预渲染逻辑中(如server.ts或预渲染配置)
const apolloState = apolloClient.cache.extract();
// 在HTML中添加该script标签,注意转义避免XSS
<script>
  window.__APOLLO_STATE__ = ${JSON.stringify(apolloState).replace(/</g, '\\u003c')};
</script>

客户端阶段

初始化Apollo时从全局变量恢复缓存:

export function createApollo(httpLink: HttpLink) {
  const cache = new InMemoryCache().restore(window.__APOLLO_STATE__ || {});

  return {
    link: httpLink.create({ uri: '/graphql' }),
    cache,
  };
}

2. 组件内优先读取缓存数据

在ngOnInit中先检查Apollo缓存是否已有目标数据,有则直接使用,无则发起请求:

ngOnInit() {
  const id = this.route.snapshot.data.id;
  // 先查询缓存
  const cachedData = this.apollo.client.cache.readQuery<MyQuery>({
    query: MyDocument,
    variables: { name: id }
  });

  if (cachedData) {
    this.contentSignal.set(cachedData);
    return;
  }

  // 缓存无数据时再发起请求
  this.apollo.runQuery<MyQuery, MyQueryVariables>({
    client: 'cms',
    document: MyDocument,
    variables: { name: id }
  }).subscribe({
    next: (data) => {
      this.contentSignal.set(data);
    },
  });
}

3. 使用Angular TransferState传递数据

如果基于Angular Universal实现预渲染,可通过TransferState在服务端和客户端间传递数据:

服务端预渲染时

将获取的数据存入TransferState:

import { TransferState, makeStateKey } from '@angular/platform-browser';

const CONTENT_KEY = makeStateKey<MyQuery>('landing-content');

// 预渲染时获取数据并存入状态
const data = await this.apollo.runQuery<MyQuery, MyQueryVariables>({...}).toPromise();
this.transferState.set(CONTENT_KEY, data);

客户端初始化时

从TransferState读取数据,避免重复请求:

ngOnInit() {
  const id = this.route.snapshot.data.id;
  const CONTENT_KEY = makeStateKey<MyQuery>('landing-content');
  
  // 优先读取TransferState中的数据
  const cachedData = this.transferState.get(CONTENT_KEY, null);
  if (cachedData) {
    this.contentSignal.set(cachedData);
    // 读取后移除状态,避免后续路由复用
    this.transferState.remove(CONTENT_KEY);
    return;
  }

  // 无数据时发起请求
  this.apollo.runQuery<MyQuery, MyQueryVariables>({...}).subscribe({
    next: (data) => {
      this.contentSignal.set(data);
    },
  });
}

内容的提问来源于stack exchange,提问作者Chrillewoodz

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.07.03 08:35:32