使用 Claude Code 进行分页:Cursor-Based vs OFFSET 与 Infinite Scroll

发布: (2026年3月11日 GMT+8 12:57)
4 分钟阅读
原文: Dev.to

Source: Dev.to

请提供您希望翻译的具体文本内容,我将按照要求将其译成简体中文并保留原始的格式、Markdown 语法以及代码块和链接。谢谢!

分页设计规则

方法

  • 超过 10,000 行:必须使用基于 cursor 的分页(禁止使用 OFFSET)
  • 小数据集的管理后台 UI:可以接受使用 OFFSET 分页
  • 无限滚动:使用基于 cursor 的分页 + hasNextPage

基于 Cursor

  • Cursor 必须是不透明的(不要向客户端暴露内部结构)
  • 将 ID 或复合键的 Base64 编码作为 cursor
  • 响应中包含 nextCursorhasNextPage

性能

  • 强制 pageSize 限制(最大 100)
  • 必须提供 orderBy(基于 cursor 的排序需要)
  • 为 cursor 列建立索引

生成游标分页

Endpoint: GET /api/users
Parameters: cursor?, limit (1‑100, default 20)
Response: { data: User[], nextCursor: string | null, hasNextPage: boolean }

Requirements

  • Prisma 游标分页
  • Base64 编码游标
  • orderBy: createdAt desc, id desc(平局时的次要排序)

生成的代码

分页工具 (src/utils/pagination.ts)

// src/utils/pagination.ts
export function encodeCursor(id: string, createdAt: Date): string {
  return Buffer.from(
    JSON.stringify({ id, createdAt: createdAt.toISOString() })
  ).toString('base64');
}

export function decodeCursor(cursor: string): { id: string; createdAt: Date } | null {
  try {
    const decoded = JSON.parse(
      Buffer.from(cursor, 'base64').toString('utf-8')
    );
    return { id: decoded.id, createdAt: new Date(decoded.createdAt) };
  } catch {
    return null;
  }
}

用户服务 (src/services/userService.ts)

// src/services/userService.ts
export async function listUsers(
  limit: number,
  cursor?: string
): Promise> {
  const take = Math.min(limit, 100);
  const decoded = cursor ? decodeCursor(cursor) : null;

  const users = await prisma.user.findMany({
    take: take + 1, // Fetch one extra to detect hasNextPage
    ...(decoded && {
      cursor: { id: decoded.id },
      skip: 1,
    }),
    orderBy: [
      { createdAt: 'desc' },
      { id: 'desc' }, // Tiebreaker for same timestamp
    ],
  });

  const hasNextPage = users.length > take;
  const data = hasNextPage ? users.slice(0, take) : users;
  const lastItem = data[data.length - 1];

  return {
    data,
    nextCursor: lastItem ? encodeCursor(lastItem.id, lastItem.createdAt) : null,
    hasNextPage,
  };
}

前端无限滚动

// useInfiniteUsers.ts
import { useInfiniteQuery } from '@tanstack/react-query';

export function useInfiniteUsers() {
  return useInfiniteQuery({
    queryKey: ['users'],
    queryFn: ({ pageParam }) =>
      fetch(`/api/users?limit=20${pageParam ? `&cursor=${pageParam}` : ''}`).then(r => r.json()),
    getNextPageParam: (lastPage) => lastPage.hasNextPage ? lastPage.nextCursor : undefined,
    initialPageParam: undefined,
  });
}

function UserList() {
  const { data, fetchNextPage, hasNextPage, isFetchingNextPage } = useInfiniteUsers();
  const ref = useRef(null);

  // Trigger next page when bottom element comes into view
  useIntersectionObserver(ref, () => {
    if (hasNextPage && !isFetchingNextPage) fetchNextPage();
  });

  return (
    <>
      {data?.pages.flatMap(p => p.data).map(user => )}
      
    
  );
}

OFFSET 分页(仅限管理员面板)

// Only when direct page jumping is required
router.get('/admin/users', async (req, res) => {
  const page = Math.max(1, Number(req.query.page) || 1);
  const limit = 20;

  const [total, users] = await prisma.$transaction([
    prisma.user.count(),
    prisma.user.findMany({
      skip: (page - 1) * limit,
      take: limit,
      orderBy: { createdAt: 'desc' },
    }),
  ]);

  res.json({
    data: users,
    total,
    page,
    totalPages: Math.ceil(total / limit),
  });
});

Summary

  • CLAUDE.md – 需要基于游标的分页以处理 10k+ 行;在大规模时禁止使用 OFFSET。
  • Base64 游标 – 对客户端隐藏内部结构。
  • take + 1 技巧 – 多取一个项目,以在不额外执行 COUNT 查询的情况下确定 hasNextPage
  • React Query useInfiniteQuery – 内置支持游标的无限滚动。
  • 代码审查套餐(¥980)包括 /code-review,用于检测分页问题——大规模使用 OFFSET、不安全的游标暴露、缺失索引。
0 浏览
Back to Blog

相关文章

阅读更多 »