分页是十分常见的接口使用场景,该篇文章详细介绍常见的分页方式,主要关注于GraphQL中分页的形式。
相关文章
- GraphQL学习:入门
- GraphQL学习:分页
目录
- 基于偏移量分页
- 基于游标分页
- Relay风格的分页
- 自定义分页格式
基于偏移量分页
查询参数:
-
offset
: 指定数据从第几个开始 -
limit
: 指定实际返回的数据个数
基于偏移量的分页实现非常简单,但是如果数据发生变化,前后两页查询可能查出相同的数据。实现如下:
// mock数据
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 } // 返回的数据个数
},
resolve (parent, { offset, limit }) {
return users.slice(offset, offset + limit)
}
}
}
})
查询前5条数据,如下:
基于偏移量的分页查询结果1从第5条数据开始查询5条数据,如下:
基于偏移量的分页查询结果2基于偏移量的分页通常直接返回数组数据,由于实现方法简单,适用于数据变化较小,不需要显示分页信息的场景。如评论区的历史评论,每次只需要基于上次的偏移量,往后加载一定数量的数据。
基于游标分页
查询参数:
-
cursor
: 当前游标 -
limit
: 指定实际返回的数据个数
基于游标分页会返回游标之后的数据,所以需要数据有明确且固定的排序规则,比如递增的id、递增的创建时间等。通常返回的数据需要指明游标,以便于下一次使用新的游标查询。
使用id作为游标
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游标
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条数据,如下:
使用id游标的分页查询使用createAt作为游标
const initialDate = moment('2019-01-01')
// mock生成递增的时间数据
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游标
limit: { type: GraphQLInt }
},
resolve (parent, { cursor, limit }) {
const offset = findIndex(users, one => one.createAt === cursor)
return users.slice(offset + 1, offset + 1 + limit)
}
}
}
})
查询前5条数据,如下:
使用createAt游标的分页查询1查询游标为'2019-01-01 05:06:53'后5条数据,如下:
使用createAt游标的分页查询2基于游标的分页可以解决因数据变化查询出相同数据的问题,适用于变化的无限列表加载。如朋友圈的动态,每次加载时基于上次加载的时间游标查询,可避免出现重复的动态,而不管之前的动态是否有变化。
Relay风格的分页
relay是facebook推出的在React中易于使用GraphQL的框架,其中有一套完整的分页解决方案。
查询参数:
-
first
: 指定取游标后的多少个数据,与after
搭配使用 -
after
: 开始游标,与first
搭配使用 -
last
: 指定取游标前的多少个数据,与before
搭配使用 -
before
: 结束游标,与last
搭配使用
relay风格的分页格式定义细节见:https://facebook.github.io/relay/graphql/connections.htm#
一个relay风格的分页实现如下:
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 UserEdge = new GraphQLObjectType({
name: 'UserEdge',
fields: {
cursor: { type: GraphQLInt }, // 每个对象必须包含游标字段
node: { type: User } // 实际的数据对象
}
})
// 分页信息
const PageInfo = new GraphQLObjectType({
name: 'PageInfo',
fields: {
// 是否有下一页,该字段必须
hasNextPage: { type: GraphQLBoolean },
// 是否有上一页,该字段必须
hasPreviousPage: { type: GraphQLBoolean },
// 总页数,根据实际情况添加
totalPageCount: { type: GraphQLInt },
// 总数据量,根据实际情况添加
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 }) {
// 起始游标和结束游标至少存在一个
if (frist == null && last == null) {
throw new Error('invalid params')
}
let data
let hasNextPage
let hasPreviousPage
const { length: total } = users
if (frist) {
// 根据起始游标和需要的数量计算
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 {
// 根据结束游标和需要的数量计算
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))
}
}
}
}
}
})
根据起始游标和需要的数量查询,如下:
根据起始游标和需要的数量查询根据结束游标和需要的数量查询,如下:
根据结束游标和需要的数量查询relay风格的分页定义的参数是比较全面的,并且可以根据需求去扩展pageInfo对象,基本上适用于所有分页场景,如列表、表格等,只是需要考虑实际场景中sql的优化。
自定义分页格式
以上介绍的几种分页方式对于表格的分页不是很常用,大部分web端的表格分页是使用page
和pageSize
来分页的。以下是自定义分页方式的实现:
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 }, // 总数量
totalPageCount: { type: GraphQLInt } // 总页数
}
})
const queryObjectType = new GraphQLObjectType({
name: 'RootQuery',
fields: {
users: {
type: UserPagination,
args: {
page: { type: GraphQLInt }, // 当前处于第几页
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的数据,如下:
根据page和pageSize查询自定义分页查询展示了常见表格分页处理的方式,这里想说明的是在GraphQL中完全可以按照适合自己前端处理的方式来定义分页格式,而不局限于常见的分页方式。
总结
本文展示了GraphQL中常见的分页方式,在实际的使用中应根据客户端的需求来选择哪种方式,如果使用了Graph的客户端框架(如relay),通常分页的方式就固定下来了,需要服务端对应去实现客户端所要求的分页参数和返回形式。
本文参考资源如下:
网友评论