搜索页的 URL 状态管理:可分享、可回退、可刷新

之前做的搜索页被用户吐槽过三件事:搜了关键词、点进结果、按返回------搜索状态没了;想把搜索结果发给同事------没链接可发;刷新页面------回到空白。三个问题的根因是同一个:搜索状态存在组件里,没同步到 URL。这篇记录把搜索页状态搬到 URL 的完整做法。

核心规则:URL 是搜索状态的唯一来源

搜索状态包括:关键词 q、页码 p、市场/语言(hl/gl)、排序、筛选。把它们全部放进 URL,收益立刻兑现:

  • 可分享:复制地址栏就是分享链接
  • 可回退:浏览器前进后退天然工作
  • 可刷新:F5 之后状态还在
  • 可 SSR:服务端能读到参数,直接渲染带结果的首屏(对 SEO 友好)

反过来说,别再用 useState 存一份------两份状态必然不同步。

读取:从 URL 派生状态

tsx 复制代码
"use client";
import { useSearchParams, useRouter, usePathname } from "next/navigation";

function useSearchState() {
  const sp = useSearchParams();
  const router = useRouter();
  const pathname = usePathname();

  // 状态全部从 URL 派生,不做本地副本
  const q = sp.get("q") ?? "";
  const page = Number(sp.get("p") ?? 1);
  const hl = sp.get("hl") ?? "zh-cn";
  const gl = sp.get("gl") ?? "cn";

  const setParams = (
    patch: Record<string, string | number | undefined>,
    mode: "push" | "replace" = "replace",
  ) => {
    const next = new URLSearchParams(sp.toString());
    for (const [k, v] of Object.entries(patch)) {
      if (v === undefined || v === "" || v === null) next.delete(k);
      else next.set(k, String(v));
    }
    const url = `${pathname}?${next.toString()}`;
    mode === "push" ? router.push(url) : router.replace(url);
  };

  return { q, page, hl, gl, setParams };
}

两个关键设计:

  • 默认值不写入 URLhl=zh-cnp=1 是默认值就删掉参数,链接短、缓存命中率高
  • replacepush 分开 :输入过程用 replace,点「搜索」用 push

写回:输入 replace,提交 push

这是最容易搞错的地方。如果每敲一个字都 push 一条历史,返回按钮就废了------用户要按 20 次才退得回去。

tsx 复制代码
function SearchInput({ value, onSubmit }: { value: string; onSubmit: (v: string) => void }) {
  const [text, setText] = useState(value);

  // 输入防抖 + replace:不污染历史
  useEffect(() => {
    const t = setTimeout(() => {
      if (text !== value) onSubmit(text.trim()); // 内部走 setParams(patch, "replace")
    }, 300);
    return () => clearTimeout(t);
  }, [text]);

  return (
    <input
      value={text}
      onChange={(e) => setText(e.target.value)}
      onKeyDown={(e) => e.key === "Enter" && onSubmit(text.trim())}
      placeholder="搜索关键词"
    />
  );
}

配合 router.replace 做草稿态,回车/点按钮时 router.push 落一个「历史锚点」------这样返回按钮退回的是上一次提交的查询,而不是上一个字符。

取数:以 URL 参数为缓存键

状态在 URL 后,数据请求直接以参数为 key,天然享受缓存:

tsx 复制代码
const key = ["/api/serp", q, page, hl, gl];
const { data, isLoading } = useSWR(key, fetcher, { keepPreviousData: true });
  • 返回上一个查询 → key 命中缓存,不重新请求
  • 翻页/切市场 → key 变化,自动请求新数据
  • keepPreviousData 让翻页时旧结果保留,不闪空白

分享与 SEO:两件小事

复制分享:一行搞定。

tsx 复制代码
<button onClick={() => navigator.clipboard.writeText(location.href)}>
  复制结果链接
</button>

服务端首屏 :分享链接被爬虫/用户打开时,希望直接看到结果。Next.js App Router 里页面组件能读到 searchParams

tsx 复制代码
export default async function Page({ searchParams }) {
  const q = searchParams.q as string | undefined;
  const initial = q ? await fetchSerp(q) : null; // 服务端调搜索数据接口
  return <SearchPage q={q} initialData={initial} />;
}

服务端取数这一步,用搜索数据接口(比如 SerpBase 的 /google/search 端点)在服务端发起,Key 天然不暴露给浏览器,首屏直接带结果。

踩坑记录

坑 1:useState + URL 双份状态。 输入框里是 state,URL 里是另一份,改一处忘一处,行为诡异。只留 URL 一份,输入框用本地草稿态 + 防抖回写。

坑 2:每敲一键 push 历史。 返回按钮要按几十次。输入用 replace,提交用 push

坑 3:忘记 encodeURIComponent 中文关键词、空格、& 直接拼进 URL 会截断或乱码。用 URLSearchParams 构造,它会正确编码。

坑 4:读取 window.location 导致水合报错。 SSR 首屏没有 window,客户端 hydration 时值不一致。用框架的 useSearchParams(SSR 安全)而不是手读 window.location

坑 5:默认值全写进 URL。 ?q=x&p=1&hl=zh-cn&gl=cn&sort=relevance------长、丑、缓存全 miss。默认值省略。

工程清单

  1. URL 是搜索状态的唯一来源,不做本地副本
  2. 输入 replace、提交 push,历史干净
  3. 防抖 300ms,别每键请求/写历史
  4. 默认值不写 URL,参数用 URLSearchParams 编码
  5. 取数以 URL 参数为缓存键,返回时命中缓存
  6. 服务端读 searchParams 渲染首屏,分享链接即可用

URL 状态管理这件事,本质上是把「页面当前显示什么」变成「地址栏里写着什么」------前者只对当前用户可见,后者可分享、可回退、可爬取。搜索页尤其适用:搜索结果本来就是「可引用」的内容

接口参数(qpagehl/gl 这些)和 URL 参数一一对应,字段说明在 SerpBase 官方文档。你们的搜索页状态存在哪,URL 还是内存?评论区聊聊踩过的坑。

相关推荐
Liaiyang661 小时前
空圈容错视角下的无人机全链路审计:从理论框架到耦合式检验
人工智能·pytorch·python·深度学习·系统架构·自动驾驶·无人机
liuchangng1 小时前
Jev 模型研究:从生成式大模型到决策式模型——System One、RLCD 校准与采用边界
java·javascript·人工智能·python·深度学习
微小冷1 小时前
patsy:Python中的统计建模语法
python·统计·建模·patsy·statmodels
我是神61 小时前
抖音直播QQ估价直播软件exe详解
python·tkinter·qq评估
IvanLiu2 小时前
我故意构造了 3 类上游失败:怎样避免 API 失败后误扣费?
api
2601_966949652 小时前
为什么量化回测中 for 循环比 DataFrame 慢三个数量级?从向量化原理讲起
开发语言·python·pandas·股票数据·quantdash
打工仔折腾 AI2 小时前
用 Docker 部署 Excalidraw 手绘白板并配置固定公网访问的完整实践
人工智能·后端·python
2601_962885724 小时前
A股数据源和 API 怎么选?(2026 全景选型指南 + 决策树)
python
Ticnix4 小时前
别再手动上线了:一条命令带备份、健康检查和自动回滚
后端·python·ci/cd