N18第 1 / 1 课时未开始内容版本 1.0.0 · 核验于 2026-10-26

搜索与分页

搜索与分页 · 阅读后完成下方实验与检查题,再用「标记完成」记录进度。

学习目标

搜索结果页要能「分享出去还是同一个结果」:关键词、排序、页码全部进 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 参数的导航。

示例拆解

本机验收:

  1. 打开 /search?q=缓存&sort=time&page=2,确认结果与 URL 三要素一致。
  2. 点「下一页」——URL 变成 page=3;浏览器后退——回到 page=2 的结果。
  3. 复制完整 URL 到新标签页打开——结果一致,分享成立。
  4. 搜一个不存在的词——零结果占位而非报错;把 page 改成 999——返回空页或回落到有效页,不抛异常。

关键点:无关键词、零结果、越界页码是搜索页的三个必答题,每一个都要有明确的界面回应,而不是空白或异常。

动手任务

在本机工程里完成:

  1. 把搜索页的关键词、排序、页码全部改为从 searchParams 读取(await 之后)。
  2. 实现保留参数的分页链接,验证「翻页 → 后退 → 前进」链路状态正确。
  3. 构造 page=abc 与 page=-1 请求,确认解析兜底为第 1 页而不是 NaN。

常见错误

  • 关键词没 encodeURIComponent 就拼 URL:含 &、空格的词直接把参数结构拆坏。
  • 用客户端 state 存排序和页码:刷新归零、无法分享,等于白做。
  • NaN 直接当页码用:Number.parseInt("abc") 返回 NaN,OFFSET 绑定 NaN 会抛错;解析后立即兜底。
  • 零结果返回空白页:用户分不清「没结果」还是「页面坏了」;零结果要有文案与下一步建议。

检查与复盘

自查清单:

  • 搜索结果 URL 粘贴到新窗口可完整复现
  • 翻页、后退、前进的状态链路验证过
  • 三种空态(无关键词、零结果、越界页码)都有对应界面

参考资料

检查题共 1 题,提交后立即显示解析
答案由服务端判定;答错的题会自动进入复习计划。
Q1故障定位中等

bug 现场:课程列表页有关键词筛选和页码。翻到第 3 页后点浏览器「后退」回到第 1 页;把当前链接发给别人,对方打开看到的也是无筛选的第 1 页。实现如下。原因和修法是?

'use client';
const [q, setQ] = useState('');
const [page, setPage] = useState(1);
// 搜索与翻页都只调用 setQ / setPage,URL 不变化

读完这一课了?

标记完成后,工作台会安排接下来的学习。

Command Palette

Search for a command to run...