Rest 分页
扩展列表
如果你想把结果追加到现有列表中,而不是跳转到另一页,只要提供了 paginationField,就可以使用 Resource.getList.getPage。
User
Post
PostItem
LoadMore
PostList
import { useController, useLoading } from '@data-client/react'; import { PostResource } from './Post'; export default function LoadMore({ cursor }: { cursor: string }) { const ctrl = useController(); const [loadPage, isPending] = useLoading( () => ctrl.fetch(PostResource.getList.getPage, { cursor }), [cursor], ); return ( <center> <button onClick={loadPage} disabled={isPending}> {isPending ? '...' : 'Load more'} </button> </center> ); }
结果
Store▶
别忘了为 Resource 定义 paginationField 以及正确的 schema!
Post
export const PostResource = resource({
path: '/posts/:id',
schema: Post,
paginationField: 'cursor',
}).extend('getList', {
schema: { posts: new Collection([Post]), cursor: '' },
});
Github Issues 演示
NextPage 组件有一个点击处理函数,它会调用 RestEndpoint.getPage。滚动到预览底部,点击 "Load more" 即可追加下一页 issue。
探索 github-app 示例
直接使用 RestEndpoint
下面我们使用 cosmos 验证者列表来探索一个真实的示例。
由于验证者只有一个 Endpoint,我们使用 RestEndpoint 而不是 resource。借助 Collections 和 paginationField,我们可以调用 RestEndpoint.getPage,将下一页验证者追加到列表中。
Validator
ValidatorItem
LoadMore
ValidatorList
import { useController, useLoading } from '@data-client/react'; import { getValidators } from './Validator'; export default function LoadMore({ next_key, limit }) { const ctrl = useController(); const [handleLoadMore, isPending] = useLoading( () => ctrl.fetch(getValidators.getPage, { 'pagination.limit': limit, 'pagination.key': next_key, }), [next_key, limit], ); if (!next_key) return null; return ( <center> <button onClick={handleLoadMore} disabled={isPending}> {isPending ? '...' : 'Load more'} </button> </center> ); }
结果
Store▶
无限滚动
由于 UI 行为差异很大,实现方式也因平台(react-native 或 web)而异,这里我们只假设已经构建好了一个 Pagination 组件,它通过回调来触发下一页的获取。在 web 上,推荐使用基于 Intersection Observers 的方案
import { useSuspense, useController } from '@data-client/react';
import { PostResource } from 'resources/Post';
import Pagination from './Pagination';
import PostList from './PostList';
function NewsList() {
const { results, cursor } = useSuspense(PostResource.getList);
const ctrl = useController();
return (
<Pagination
onPaginate={() =>
ctrl.fetch(PostResource.getList.getPage, { cursor })
}
>
<PostList posts={results} />
</Pagination>
);
}
HTTP 头中的 token
在某些情况下,分页 token 会嵌在 HTTP 头中,而不是作为响应体的一部分。这时你需要为 getList 自定义 parseResponse() 函数,让分页头包含在获取到的对象中。
下面展示自定义的 getList。上述示例的其余部分保持不变。
在本例中,分页 token 存储在 link 头中。
import { Collection, Resource } from '@data-client/rest';
export const ArticleResource = resource({
path: '/articles/:id',
schema: Article,
}).extend(Base => ({
getList: Base.getList.extend({
schema: { results: [Article], link: '' },
async parseResponse(response: Response) {
const results = await Base.getList.parseResponse(response);
if (
(response.headers && response.headers.has('link')) ||
Array.isArray(results)
) {
return {
link: response.headers.get('link'),
results,
};
}
return results;
},
}),
}));
代码组织
如果你的大部分 API 都采用类似的分页方式,可以尝试用一个自定义的 Endpoint 类来共享这段逻辑。
resources/PagingEndpoint.ts
import { Collection, RestEndpoint, type RestGenerics } from '@data-client/rest';
export class PagingEndpoint<
O extends RestGenerics = any,
> extends RestEndpoint<O> {
async parseResponse(response: Response) {
const results = await super.parseResponse(response);
if (
(response.headers && response.headers.has('link')) ||
Array.isArray(results)
) {
return {
link: response.headers.get('link'),
results,
};
}
return results;
}
}
resources/MyResource.ts
import { Collection, Entity, resource } from '@data-client/rest';
import { PagingEndpoint } from './PagingEndpoint';
export const MyResource = resource({
path: '/stuff/:id',
schema: MyEntity,
Endpoint: PagingEndpoint,
});