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

