【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载本篇以 HowToGraphQL 教程中 Angular Apollo 克隆 Hacker News 项目的分页章节为主线讲解如何基于 GraphQL 的first/skip/orderBy参数实现 limit-offset 分页从调整路由让LinkListComponent同时承载「热门 Top」与「按页浏览新帖」两种职责到用 RxJS 可观察流把 URL 参数动态映射为查询变量再到分页引入后readQuery/writeQuery必须携带变量以维持缓存一致性。读完本篇你能够在一个已集成 Apollo Client 的 Angular 应用中完整落地一套可分页、可排序、且缓存可更新的链接列表方案。分页背景为什么需要 Limit-Offset该教程章节大纲的最终目标是构建一个 Hacker News 克隆列表展示链接、搜索、登录、发帖、点赞、实时订阅。在此之前各章已经实现了查询、变更mutation、路由、筛选与订阅参见 Subscriptions 章节。分页是本教程前端线的收尾主题当Link元素数量增长后一次拉全量列表会渲染出极长的页面。因此本章实现一个简单的分页方案让用户以更小的数据块chunk浏览链接而不必一次性加载全部Link。方案选型说明本教程采用的是 limit-offset 分页与 SQL 中LIMIT/OFFSET思路一致。后端多语言教程中均指出这种方式不适用于 Relay 前端因为 Relay 强制要求基于游标cursor的 connections 分页规范可对比 graphql-java 的分页说明 与 graphql-python 的 Relay 章节。对本教程使用的 Apollo Client 而言limit-offset 已足够且实现直观。本章所有代码都落在hackernews-angular-apollo这个由读者用 angular-cli 创建的项目内本文仅说明文件路径供对照该工程本身不在本仓库中。调整路由让 LinkListComponent 承担两种职责分页的第一步是微调现有路由。核心想法是LinkListComponent将服务两条路由、两种使用场景展示投票最高的 10 条链接/top将新链接按多页切分展示用户可在页间导航/new/:page。打开src/app/app.routing.ts将路由调整为如下结构新增的/top与/new/:page两条路由是改动重点routes: Routes [ { path: , pathMatch: full, redirectTo: /new/1 }, { path: new/:page, component: LinkListComponent, pathMatch: full }, { path: top, component: LinkListComponent, pathMatch: full }, { path: create, component: CreateLinkComponent, pathMatch: full }, { path: login, component: LoginComponent, pathMatch: full }, { path: search, component: SearchComponent, pathMatch: full }, { path: **, redirectTo: , } ];要点解析new/:page中的:page是路由参数其值会从 URL 中被读出并在所渲染的组件这里是LinkListComponent内可用——这是后续所有分页计算的数据来源根路由现在重定向到/new/1即新帖列表的第一页pathMatch: full保证这些路由只在完整匹配时才生效与末尾的**通配重定向配合避免误匹配。同时需要在HeaderComponent中暴露新的/top入口。打开src/app/header/header.component.html模板应如下新增的top链接位于 new 与 search 之间div classflex pa1 justify-between nowrap orange div classflex flex-fixed black div classfw7 mr1Hacker News/div a [routerLink][/] classml1 no-underline blacknew/a div classml1|/div a [routerLink][/top] classml1 no-underline blacktop/a div classml1|/div a [routerLink][/search] classml1 no-underline blacksearch/a div classflex *ngIflogged div classml1|/div a [routerLink][/create] classml1 no-underline blacksubmit/a /div /div div classflex flex-fixed div *ngIflogged classml1 pointer black (click)logout()logout/div a *ngIf!logged [routerLink][/login] classml1 no-underline blacklogin/a /div /div升级 ALL_LINKS_QUERY引入 first、skip、orderBy 与元数据 countLinkListComponent现在需要承担两种职责因此要为它补充相当多的逻辑。第一步是升级查询本身。打开src/app/graphql.ts用下面的定义替换ALL_LINKS_QUERY——注意新增的三个变量以及_allLinksMeta中额外请求的count字段用于获取链接总数是计算「是否还有下一页」的前提export const ALL_LINKS_QUERY gql query AllLinksQuery($first: Int, $skip: Int, $orderBy: LinkOrderBy) { allLinks(first: $first, skip: $skip, orderBy: $orderBy) { id createdAt url description postedBy { id name } votes { id user { id } } } _allLinksMeta { count } } ; export interface AllLinkQueryResponse { allLinks: Link[]; _allLinksMeta: { count: number }; loading: boolean; }三个参数的语义skip定义查询起始的offset。例如传10表示列表前 10 项不会出现在响应中first定义limit即从 offset 处开始取多少条元素。例如skip: 10、first: 5时拿到的是列表第 10 到第 15 项orderBy排序方式LinkOrderBy枚举本教程用于/new页按createdAt_DESC最新在前排序。由于查询签名变了所有引用它的地方都必须同步传入变量。先更新CreateLinkComponent中 mutation 的update回调打开src/app/create-link/create-link.component.ts让update如下本质是给readQuery/writeQuery补上查询所期望的变量update: (store, { data: { createLink } }) { const data: any store.readQuery({ query: ALL_LINKS_QUERY, variables: { first: 5, skip: 0, orderBy: createdAt_DESC } }); data.allLinks.push(createLink); store.writeQuery({ query: ALL_LINKS_QUERY, variables: { first: 5, skip: 0, orderBy: createdAt_DESC }, data }) },LinkListComponent 核心用可观察流把 URL 映射为查询变量接下来打开src/app/link-list/link-list.component.ts重写watchQuery的构建逻辑。这是本章节最核心的实现——它把「路由参数」变成「查询变量」的响应式流// 0 const pageParams$: Observablenumber this.route.paramMap .map((params) { return parseInt(params.get(page), 10); }); // 1 const path$: Observablestring this.route.url .map((segments) segments.toString()); // 2 this.first$ path$ .map((path) { const isNewPage path.includes(new); return isNewPage ? this.linksPerPage : 100; }); // 3 this.skip$ Observable.combineLatest(path$, pageParams$) .map(([path, page]) { const isNewPage path.includes(new); return isNewPage ? (page - 1) * this.linksPerPage : 0; }); // 4 this.orderBy$ path$ .map((path) { const isNewPage path.includes(new); return isNewPage ? createdAt_DESC : null; }); // 5 const getQuery (variables): ObservableApolloQueryResultAllLinkQueryResponse { const query this.apollo.watchQueryAllLinkQueryResponse({ query: ALL_LINKS_QUERY, variables }); // Call .subscribeToMore on the query for NEW_LINKS_SUBSCRIPTION, NEW_VOTES_SUBSCRIPTION omitted return query.valueChanges; }; const allLinkQuery: ObservableApolloQueryResultAllLinkQueryResponse Observable // 6 .combineLatest(first$, skip$, orderBy$, (first, skip, orderBy) ({ first, skip, orderBy })) // 7 .switchMap((variables: any) getQuery(variables)); // 8 const querySubscription allLinkQuery.subscribe((response) { this.allLinks response.data.allLinks; this.count response.data._allLinksMeta.count; this.loading response.data.loading; });逐步拆解这 9 个步骤0–8pageParams$基于this.route.paramMap构建取出全部路由参数后 map 出page参数并parseInt为数字path$类似地基于this.route.url构建把UrlSegment序列转成字符串用于判断当前在/new还是/top路由上first$第一个将作为watchQuery变量的流。first$决定每次取回的链接块大小/new页取linksPerPage条/top页一次取 100 条之后在本地排序截取前 10skip$第二个变量流由path$与pageParams$合并计算。/new页的 offset 是(page - 1) * linksPerPage/top页恒为 0orderBy$/new页使用createdAt_DESC保证最新链接排在最前/top页传null因为其排序将基于每条链接的票数在本地手动计算getQuery工厂函数接收变量first$、skip$、orderBy$的值用它们调用watchQuery并返回valueChanges流这里省略的subscribeToMore调用用于在查询上挂载NEW_LINKS_SUBSCRIPTION与NEW_VOTES_SUBSCRIPTION保持列表实时刷新详见 Subscriptions 章节合并变量Observable.combineLatest把first$、skip$、orderBy$三个流合并产出{ first, skip, orderBy }对象switchMap展平getQuery返回的是ObservableApolloQueryResult...若用.map会得到ObservableObservable...的二层嵌套因此用switchMap将其展平为单层查询结果流。switchMap的额外好处是当 URL 变化触发新变量时上一个查询订阅会自动被取消并切换到新查询天然避免竞态订阅落地执行查询并把allLinks与_allLinksMeta.count存入组件属性。这里的设计值得强调查询变量并非在ngOnInit里一次性算好的静态值而是跟随路由变化的响应式流。用户点击「下一页」只是改变了 URLparamMap发出新值skip$随之重算switchMap自动切换到新的分页查询——组件无需手写任何「监听 URL → 重新请求」的胶水代码。每页条数常量与组件属性定义每页条数常量。打开src/app/constants.ts添加export const LINKS_PER_PAGE 5;然后在LinkListComponent中补充属性声明与导入// src/app/link-list/link-list.component.ts 中新增的变量声明 first$: Observablenumber; skip$: Observablenumber; orderBy$: Observablestring | null;import { LINKS_PER_PAGE } from ../app/constants;并把linksPerPage映射到常量上linksPerPage LINKS_PER_PAGE;同时新增用于模板判断的总数属性count 0;实现页间导航按钮、getter 与有序链接接下来实现用户在页之间切换的能力。先更新src/app/link-list/link-list.component.html模板在列表底部加入两个导航按钮h4 *ngIfloadingLoading.../h4 hn-link-item *ngForlet link of (orderedLinks| async);let indexindex [link]link [index]index [pageNumber]pageNumber | async [isAuthenticated]logged /hn-link-item div *ngIfisNewPage | async button *ngIf!(isFirstPage | async) (click)previousPage()Previous/button button *ngIfmorePages| async (click)nextPage()Next/button /div两个要点列表渲染的是orderedLinks——LinkListComponent中即将新增的一个 getter它按路由决定是原样返回查询结果还是按票数排序async管道AsyncPipe会订阅 Observable 或 Promise 并返回其最新一次发出的值。注意模板中每多使用一次 async 管道就会对底层流多触发一次订阅。由于这里底层都是路由流本地计算、无网络开销成本可接受但这是理解模板行为的关键。模板把pageNumber作为hn-link-item的Input传入因此需要给LinkItemComponent加上对应输入// src/app/link-item/link-item.component.ts Input() pageNumber: number 0;由于「列表如何呈现」变复杂了渲染的链接列表改为独立方法计算。isNewPage场景下直接返回查询结果即可无需本地改动若用户从/top路由进入则按票数排序并返回前 10 条。这里会用到 lodash 库先在项目目录安装npm install --save lodash # 或者 yarn add lodash然后在src/app/link-list/link-list.component.ts中导入并实现orderedLinksgetterimport _ from lodash get orderedLinks(): ObservableLink[] { return this.route.url .map((segments) segments.toString()) .map(path { if (path.includes(top)) { return _.orderBy(this.allLinks, votes.length).reverse() } else { return this.allLinks } }) }_.orderBy按每条链接的votes.length票数排序reverse()保证票数多的在前。这解释了为什么/top页查询时orderBy传null且first取 100——排序与截断都在客户端完成。还需要四个 getter 支撑按钮的显隐逻辑get isFirstPage(): Observableboolean { return this.route.paramMap .map((params) { return parseInt(params.get(page), 10); }) .map(page page 1) } get isNewPage(): Observableboolean { return this.route.url .map((segments) segments.toString()) .map(path path.includes(new)) } get pageNumber(): Observablenumber { return this.route.paramMap .map((params) { return parseInt(params.get(page), 10); }); } get morePages(): Observableboolean { return this.pageNumber.map(pageNumber pageNumber this.count / this.linksPerPage); }语义逐一对应isFirstPagepage 1时隐藏「Previous」按钮isNewPage仅在/new/:page路由上才渲染分页按钮/top页不分页pageNumber当前页码来自 URL 参数morePagespageNumber count / linksPerPage时显示「Next」按钮。这里的count正是前面从_allLinksMeta里取回的链接总数——没有这个元数据客户端无法判断是否还有下一页。最后在LinkListComponent中实现两个按钮的处理方法nextPage () { const page parseInt(this.$route.params.page, 10) if (page this.count / LINKS_PER_PAGE) { const nextPage page 1 this.$router.push({path: /new/${nextPage}}) } }, previousPage () { const page parseInt(this.$route.params.page, 10) if (page 1) { const previousPage page - 1 this.$router.push({path: /new/${previousPage}}) } }实现思路很直接从 URL 取当前页做合法性检查页码是否在可翻页范围内然后计算目标页并让 router 导航过去。路由变化会更新 URL 中的page而pageParams$流又会驱动整条查询链重新计算正确的链接块。教程原文此处沿用了$route/$router的写法在 Angular 语境下等价的做法是注入ActivatedRoute与Router分别用route.params取参数、用router.navigate([/new, nextPage])导航——这也正是本教程配套的章节测验所考察的知识点获取当前页面 URL 信息的方式是注入ActivatedRoute服务并使用其params方法。完成以上改动后即可在运行的应用里用新按钮在链接列表间翻页。收尾调整让缓存读写与分页变量保持一致对ALL_LINKS_QUERY的升级带来一个连锁影响各 mutation 的update函数原先不再工作因为readQuery现在同样需要传入之前定义的变量。原理提示readQuery的工作方式与你在搜索功能中用过的ApolloClient上的query方法一致不同之处在于它不发起网络请求而是直接在本地缓存store上解析查询。如果一个查询是用变量从服务器取回的readQuery也必须知道这些变量才能从缓存中取出正确的那份数据——Apollo 的缓存是按「查询 变量」为键组织条目的。首先更新LinkListComponent中的updateStoreAfterVote方法updateStoreAfterVote (store, createVote, linkId) { let variables; Observable .combineLatest(this.first$, this.skip$, this.orderBy$, (first, skip, orderBy) ({ first, skip, orderBy })) .take(1) .subscribe(values variables values); // 1 const data store.readQuery({ query: ALL_LINKS_QUERY, variables }); // 2 const votedLink data.allLinks.find(link link.id linkId); votedLink.votes createVote.link.votes; // 3 store.writeQuery({ query: ALL_LINKS_QUERY, variables, data }) }这里做的事是根据用户当前在/top还是/new路由上计算出对应的变量.take(1)从合并流中取一次最新值然后用同一组变量去读缓存、修改被赞链接的votes、再写回缓存。最后调整创建新链接时的update实现。打开CreateLinkComponent将createLink方法替换为createLink() { const postedById localStorage.getItem(GC_USER_ID); if (!postedById) { console.error(No user logged in); return } const newDescription this.description; const newUrl this.url; this.description ; this.url ; const createMutationSubscription this.apollo.mutateCreateLinkMutationResponse({ mutation: CREATE_LINK_MUTATION, variables: { description: newDescription, url: newUrl, postedById }, update: (store, { data: { createLink } }) { const data: any store.readQuery({ query: ALL_LINKS_QUERY, variables: { first: LINKS_PER_PAGE, skip: 0, orderBy: createdAt_DESC } }); let allLinks data.allLinks.slice(); allLinks.splice(0, 0, createLink); allLinks.pop(); data.allLinks allLinks; store.writeQuery({ query: ALL_LINKS_QUERY, variables: { first: LINKS_PER_PAGE, skip: 0, orderBy: createdAt_DESC }, data }) }, }).subscribe((response) { // We injected the Router service this.router.navigate([/]); }, (error) { console.error(error); this.description newDescription; this.url newUrl; }); this.subscriptions [...this.subscriptions, createMutationSubscription]; }这里缓存更新的细节很讲究由于/new/1页固定显示first: LINKS_PER_PAGE, skip: 0, orderBy: createdAt_DESC的查询结果新链接插入后应当保持在窗口大小内因此代码先slice()复制数组、splice(0, 0, createLink)在头部插入新链接再pop()弹出数组末尾的旧链接——精确模拟了服务端「按创建时间倒序取前 N 条」的结果。最后别忘了在文件顶部导入常量import { GC_USER_ID, LINKS_PER_PAGE } from ../constants至此应用已具备一套简单的分页系统用户分块加载链接而不必一次性拉取全部数据。小结本方案的要点与适用边界回顾整章实现可以提炼出三个可复用的模式路由参数驱动查询变量page参数 →paramMap流 →combineLatest合成{ first, skip, orderBy }→switchMap(watchQuery)让「翻页」退化为一次导航操作数据刷新完全由响应式链路自动完成元数据支撑分页 UI_allLinksMeta.count是判断morePages的必要输入没有总数就无法决定「Next」按钮的显隐缓存操作必须与查询变量对齐Apollo 缓存以「查询 变量」为键readQuery/writeQuery缺变量或变量不匹配都会读错数据或直接失败——这是从「无分页查询」升级到「参数化查询」后最常见的坑。适用边界方面limit-offset 分页在数据变化剧烈或列表极深时大 offset 扫描可能不是最优解Relay 生态要求 cursor-based connections本教程后端Graphcool 托管 API已原生提供first/skip/orderBy参数故前端无需额外的服务端改造。若你想对比其他框架的同类实现可参考 React Apollo 的分页章节 或 Vue Apollo 的分页章节思路与本节一致差异仅在框架的路由与状态绑定方式上。注本篇整理自 HowToGraphQL 教程仓库中 content/frontend/angular-apollo/9-pagination.md该文档的 front matter 中附带了本章测验「如何访问当前页面 URL 的信息」本仓库的 Quiz 组件 会基于这些字段在每章末尾渲染问答并解锁下一章Markdown 渲染层 则负责把Instruction等自定义步骤块渲染为教程页面。赞分享【免费下载链接】howtographqlThe Fullstack Tutorial for GraphQL项目地址https://gitcode.com/gh_mirrors/ho/howtographql点击查看免费下载相关推荐TiDB分页查询LIMIT OFFSET高效实现TiDB分页查询LIMIT OFFSET高效实现 分页查询性能痛点 你是否遇到过这样的情况在处理百万级数据时使用 LIMIT 1000000, 10 查询数据库分布式数据库后端OLAPhowtographql 中 Angular Apollo 实战用 GraphQL Subscriptions 实现 Hacker News 克隆的实时更新howtographql 中 Angular Apollo 实战用 GraphQL Subscriptions 实现 Hacker News 克隆的实时更Angular Apollo 实战在 howtographql Hacker News 教程中实现路由导航与程序化跳转Angular Apollo 实战在 howtographql Hacker News 教程中实现路由导航与程序化跳转 本篇指南基于 howtograph上一篇Cangjie-SIG/fountain数据单位DataUnit计量单位下一篇FlashAI/Qwen3 插件扩展机制功能增强与自定义开发创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
企业数字化 ERP 产品动态
相关推荐
猫抓 cat-catch 使用指南:如何从网页中嗅探并下载视频与 M3U8 流 猫抓 cat-catch 使用指南:如何从网页中嗅探并下载视频与 M3U8 流 【免费下载链接】cat-catch 猫抓 浏览器资源嗅探扩展 / cat-catch Browser Resource Sniffing Extension 项目地址: https://gitcode.com/GitHub_Trending/ca/cat-catch
你在网页上看到一个想… · 2026/9/25 2:43:43
html-anything 像素动画技能实战:用纯 CSS 打造 8-bit 复古解说帧 AI 应用人工智能AI AgentAI 写作媒体生成 【免费下载链接】html-anything ✨ The agentic HTML editor — your local AI agent writes the HTML, you ship it. 🚀 75 Skills 9 Surfaces (magazine deck poster XHS / tweet prototype data report Hyperfram… · 2026/9/25 2:43:43
AutoClicker配置持久化与日志机制详解:System.Text.Json与Serilog完整实现走读 AutoClicker配置持久化与日志机制详解:System.Text.Json与Serilog完整实现走读 【免费下载链接】AutoClicker AutoClicker is a useful simple tool for automating mouse clicks. 项目地址: https://gitcode.com/gh_mirrors/au/AutoClicker
AutoClicker 是一… · 2026/9/25 3:13:05
计算机工程1区SCI三个月录用:选题、写作与投稿实操指南 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 3:13:05
基于 PaddleSeg 的多标签语义分割完整实战指南:从数据转换到训练、评估与推理 人工智能计算机视觉预训练 【免费下载链接】PaddleSeg Easy-to-use image segmentation library with awesome pre-trained model zoo, supporting wide-range of practical tasks in Semantic Segmentation, Interactive Segmentation, Panoptic Segmentation, Image Matting,… · 2026/9/25 3:12:59
创维E900V22D刷机全攻略:S905L3SB芯片兼容性解析与救砖实战 /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:31
MQTT协议原理与Broker服务器搭建实战:从Mosquitto到EMQX /* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views … · 2026/9/25 1:00:37