分頁是十分常見的接口使用場景,該篇文章詳細(xì)介紹常見的分頁方式,主要關(guān)注于GraphQL中分頁的形式。
相關(guān)文章
- GraphQL學(xué)習(xí):入門
- GraphQL學(xué)習(xí):分頁
- GraphQL學(xué)習(xí):接口、聯(lián)合類型、輸入類型
目錄
- 基于偏移量分頁
- 基于游標(biāo)分頁
- Relay風(fēng)格的分頁
- 自定義分頁格式
基于偏移量分頁
查詢參數(shù):
-
offset: 指定數(shù)據(jù)從第幾個(gè)開始 -
limit: 指定實(shí)際返回的數(shù)據(jù)個(gè)數(shù)
基于偏移量的分頁實(shí)現(xiàn)非常簡單,但是如果數(shù)據(jù)發(fā)生變化,前后兩頁查詢可能查出相同的數(shù)據(jù)。實(shí)現(xiàn)如下:
// mock數(shù)據(jù)
const users = Mock.mock({
'list|100': [{
'id|+1': 1000,
'name': /user-[a-zA-Z]{4}/
}]
}).list
// 定義User類型
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(User),
args: {
offset: { type: GraphQLInt }, // 偏移量
limit: { type: GraphQLInt } // 返回的數(shù)據(jù)個(gè)數(shù)
},
resolve (parent, { offset, limit }) {
return users.slice(offset, offset + limit)
}
}
}
})
查詢前5條數(shù)據(jù),如下:

從第5條數(shù)據(jù)開始查詢5條數(shù)據(jù),如下:

基于偏移量的分頁通常直接返回?cái)?shù)組數(shù)據(jù),由于實(shí)現(xiàn)方法簡單,適用于數(shù)據(jù)變化較小,不需要顯示分頁信息的場景。如評(píng)論區(qū)的歷史評(píng)論,每次只需要基于上次的偏移量,往后加載一定數(shù)量的數(shù)據(jù)。
基于游標(biāo)分頁
查詢參數(shù):
-
cursor: 當(dāng)前游標(biāo) -
limit: 指定實(shí)際返回的數(shù)據(jù)個(gè)數(shù)
基于游標(biāo)分頁會(huì)返回游標(biāo)之后的數(shù)據(jù),所以需要數(shù)據(jù)有明確且固定的排序規(guī)則,比如遞增的id、遞增的創(chuàng)建時(shí)間等。通常返回的數(shù)據(jù)需要指明游標(biāo),以便于下一次使用新的游標(biāo)查詢。
使用id作為游標(biāo)
const users = Mock.mock({
'list|100': [{
'id|+1': 1000,
'name': /user-[a-zA-Z]{4}/
}]
}).list
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(User),
args: {
cursor: { type: GraphQLInt }, // id游標(biāo)
limit: { type: GraphQLInt }
},
resolve (parent, { cursor, limit }) {
const offset = findIndex(users, one => one.id === cursor)
return users.slice(offset + 1, offset + 1 + limit)
}
}
}
})
查詢id為1003的用戶后5條數(shù)據(jù),如下:

使用createAt作為游標(biāo)
const initialDate = moment('2019-01-01')
// mock生成遞增的時(shí)間數(shù)據(jù)
const users = Mock.mock({
'list|100': [{
'id|+1': /[a-zA-Z0-9]{10}/,
'name': /user-[a-zA-Z]{4}/,
'createAt': () => initialDate
.add(Mock.Random.integer(1000, 10000), 'seconds')
.format('YYYY-MM-DD HH:mm:ss')
}]
}).list
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLString },
name: { type: GraphQLString },
createAt: { type: GraphQLString }
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: new GraphQLList(User),
args: {
cursor: { type: GraphQLString }, // createAt游標(biāo)
limit: { type: GraphQLInt }
},
resolve (parent, { cursor, limit }) {
const offset = findIndex(users, one => one.createAt === cursor)
return users.slice(offset + 1, offset + 1 + limit)
}
}
}
})
查詢前5條數(shù)據(jù),如下:

查詢游標(biāo)為'2019-01-01 05:06:53'后5條數(shù)據(jù),如下:

基于游標(biāo)的分頁可以解決因數(shù)據(jù)變化查詢出相同數(shù)據(jù)的問題,適用于變化的無限列表加載。如朋友圈的動(dòng)態(tài),每次加載時(shí)基于上次加載的時(shí)間游標(biāo)查詢,可避免出現(xiàn)重復(fù)的動(dòng)態(tài),而不管之前的動(dòng)態(tài)是否有變化。
Relay風(fēng)格的分頁
relay是facebook推出的在React中易于使用GraphQL的框架,其中有一套完整的分頁解決方案。
查詢參數(shù):
-
first: 指定取游標(biāo)后的多少個(gè)數(shù)據(jù),與after搭配使用 -
after: 開始游標(biāo),與first搭配使用 -
last: 指定取游標(biāo)前的多少個(gè)數(shù)據(jù),與before搭配使用 -
before: 結(jié)束游標(biāo),與last搭配使用
relay風(fēng)格的分頁格式定義細(xì)節(jié)見:https://facebook.github.io/relay/graphql/connections.htm#
一個(gè)relay風(fēng)格的分頁實(shí)現(xiàn)如下:
const users = Mock.mock({
'list|100': [{
'id|+1': 1000,
'name': /user-[a-zA-Z]{4}/
}]
}).list
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
})
// 實(shí)際返回的數(shù)據(jù)對(duì)象
const UserEdge = new GraphQLObjectType({
name: 'UserEdge',
fields: {
cursor: { type: GraphQLInt }, // 每個(gè)對(duì)象必須包含游標(biāo)字段
node: { type: User } // 實(shí)際的數(shù)據(jù)對(duì)象
}
})
// 分頁信息
const PageInfo = new GraphQLObjectType({
name: 'PageInfo',
fields: {
// 是否有下一頁,該字段必須
hasNextPage: { type: GraphQLBoolean },
// 是否有上一頁,該字段必須
hasPreviousPage: { type: GraphQLBoolean },
// 總頁數(shù),根據(jù)實(shí)際情況添加
totalPageCount: { type: GraphQLInt },
// 總數(shù)據(jù)量,根據(jù)實(shí)際情況添加
totalCount: { type: GraphQLInt }
}
})
const UserConnection = new GraphQLObjectType({
name: 'UserConnection',
fields: {
edges: { type: new GraphQLList(UserEdge) },
pageInfo: { type: PageInfo }
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: UserConnection,
args: {
frist: { type: GraphQLInt },
after: { type: GraphQLInt },
last: { type: GraphQLInt },
before: { type: GraphQLInt }
},
resolve (parent, { frist, after, last, before }) {
// 起始游標(biāo)和結(jié)束游標(biāo)至少存在一個(gè)
if (frist == null && last == null) {
throw new Error('invalid params')
}
let data
let hasNextPage
let hasPreviousPage
const { length: total } = users
if (frist) {
// 根據(jù)起始游標(biāo)和需要的數(shù)量計(jì)算
const index = findIndex(users, one => one.id === after)
data = users.slice(index + 1, index + 1 + frist)
hasNextPage = index + 1 + frist < total
hasPreviousPage = index > 0
} else {
// 根據(jù)結(jié)束游標(biāo)和需要的數(shù)量計(jì)算
const index = findIndex(users, one => one.id === before)
data = users.slice(Math.max(index - last, 0), index)
hasNextPage = index + 1 < total
hasPreviousPage = index - last > 0
}
return {
edges: data.map(one => ({ node: one, cursor: one.id })),
pageInfo: {
hasNextPage,
hasPreviousPage,
totalCount: total,
totalPageCount: Math.ceil(total / (frist || last))
}
}
}
}
}
})
根據(jù)起始游標(biāo)和需要的數(shù)量查詢,如下:

根據(jù)結(jié)束游標(biāo)和需要的數(shù)量查詢,如下:

relay風(fēng)格的分頁定義的參數(shù)是比較全面的,并且可以根據(jù)需求去擴(kuò)展pageInfo對(duì)象,基本上適用于所有分頁場景,如列表、表格等,只是需要考慮實(shí)際場景中sql的優(yōu)化。
自定義分頁格式
以上介紹的幾種分頁方式對(duì)于表格的分頁不是很常用,大部分web端的表格分頁是使用page和pageSize來分頁的。以下是自定義分頁方式的實(shí)現(xiàn):
const users = Mock.mock({
'list|100': [{
'id|+1': 1000,
'name': /user-[a-zA-Z]{4}/
}]
}).list
const User = new GraphQLObjectType({
name: 'User',
fields: {
id: { type: GraphQLInt },
name: { type: GraphQLString }
}
})
const UserPagination = new GraphQLObjectType({
name: 'UserPagination',
fields: {
data: { type: new GraphQLList(User) },
totalCount: { type: GraphQLInt }, // 總數(shù)量
totalPageCount: { type: GraphQLInt } // 總頁數(shù)
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: UserPagination,
args: {
page: { type: GraphQLInt }, // 當(dāng)前處于第幾頁
pageSize: { type: GraphQLInt } // 分頁大小
},
resolve (parent, { page, pageSize }) {
const data = users.slice((page - 1) * pageSize, page * pageSize)
const { length: total } = users
return {
data,
totalCount: total,
totalPageCount: Math.ceil(total / pageSize)
}
}
}
}
})
查詢第3頁,分頁大小為5的數(shù)據(jù),如下:

自定義分頁查詢展示了常見表格分頁處理的方式,這里想說明的是在GraphQL中完全可以按照適合自己前端處理的方式來定義分頁格式,而不局限于常見的分頁方式。
總結(jié)
本文展示了GraphQL中常見的分頁方式,在實(shí)際的使用中應(yīng)根據(jù)客戶端的需求來選擇哪種方式,如果使用了Graph的客戶端框架(如relay),通常分頁的方式就固定下來了,需要服務(wù)端對(duì)應(yīng)去實(shí)現(xiàn)客戶端所要求的分頁參數(shù)和返回形式。
本文參考資源如下: