搜索与分页
搜索与分页 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。
学习目标
搜索结果页要能「分享出去还是同一个结果」:关键词、排序、页码全部进 URL。本课把搜索页做成可分享、可后退、翻页不丢状态的完整形态,并覆盖三种空态。
问题场景
你在搜索框输入「缓存」,切了「按时间排序」,翻到第 3 页,把地址栏链接发给同事——他打开是第 1 页、默认排序,什么都对不上。状态活在组件 state 里就是这种下场:URL 不变,页面千变万化。可分享的搜索页,状态必须活在 URL 里。
核心解释
App Router 的做法:搜索状态全放 searchParams,页面从 URL 读一切。记住 N04 的规则——searchParams 是 Promise,服务端组件里要 await:
// app/search/page.tsx
import Link from "next/link";
export const dynamic = "force-dynamic";
interface SearchParams {
q?: string;
sort?: string;
page?: string;
}
export default async function SearchPage({
searchParams,
}: {
searchParams: Promise<SearchParams>;
}) {
const sp = await searchParams;
const q = (sp.q ?? "").trim();
const sort = sp.sort === "time" ? "time" : "relevance";
const page = Math.max(1, Number.parseInt(sp.page ?? "1", 10) || 1);
if (!q) return <p>输入关键词开始搜索。</p>;
const { hits, total } = searchAll(q, sort, page); // SQL 内 LIMIT/OFFSET 分页
if (hits.length === 0) {
return <p>没有找到与「{q}」相关的内容,换个关键词试试。</p>;
}
const buildHref = (p: number) =>
`/search?q=${encodeURIComponent(q)}&sort=${sort}&page=${p}`;
return (
<div>
<p>「{q}」共 {total} 条结果,第 {page} 页</p>
<ul>
{hits.map((h) => (
<li key={h.id}>{h.title}</li>
))}
</ul>
<Link href={buildHref(page - 1)} aria-disabled={page <= 1}>上一页</Link>
<Link href={buildHref(page + 1)} aria-disabled={page * 10 >= total}>下一页</Link>
</div>
);
}
为什么这个形态好:
- URL 即状态:复制地址栏给任何人,看到的是同一组关键词、排序、页码。
- 后退即恢复:翻页是路由跳转,浏览器历史自然记录,返回键回到上一页结果。
- 服务端渲染即所得:结果直接进 HTML,不需要客户端再 fetch 等一次。
分页链接用 Link 而不是 onClick 改 state:前者让 URL、历史记录、预取全部免费获得。排序切换同理——它就是一次带新 sort 参数的导航。
示例拆解
本机验收:
- 打开
/search?q=缓存&sort=time&page=2,确认结果与 URL 三要素一致。 - 点「下一页」——URL 变成 page=3;浏览器后退——回到 page=2 的结果。
- 复制完整 URL 到新标签页打开——结果一致,分享成立。
- 搜一个不存在的词——零结果占位而非报错;把 page 改成 999——返回空页或回落到有效页,不抛异常。
关键点:无关键词、零结果、越界页码是搜索页的三个必答题,每一个都要有明确的界面回应,而不是空白或异常。
动手任务
在本机工程里完成:
- 把搜索页的关键词、排序、页码全部改为从 searchParams 读取(await 之后)。
- 实现保留参数的分页链接,验证「翻页 → 后退 → 前进」链路状态正确。
- 构造 page=abc 与 page=-1 请求,确认解析兜底为第 1 页而不是 NaN。
常见错误
- 关键词没 encodeURIComponent 就拼 URL:含
&、空格的词直接把参数结构拆坏。 - 用客户端 state 存排序和页码:刷新归零、无法分享,等于白做。
- NaN 直接当页码用:
Number.parseInt("abc")返回 NaN,OFFSET 绑定 NaN 会抛错;解析后立即兜底。 - 零结果返回空白页:用户分不清「没结果」还是「页面坏了」;零结果要有文案与下一步建议。
检查与复盘
自查清单:
- 搜索结果 URL 粘贴到新窗口可完整复现
- 翻页、后退、前进的状态链路验证过
- 三种空态(无关键词、零结果、越界页码)都有对应界面
参考资料
- Next.js 文档:page 文件约定 —— searchParams 的类型与 Promise 行为
- Next.js 文档:Link 组件 —— 导航、预取与状态保持
- Next.js 文档:useSearchParams —— 客户端读取搜索参数
读完这一课了?
标记完成后,工作台会安排接下来的学习。