前端本地存搜索数据:IndexedDB 入门实战(缓存、历史、离线)

搜索工具的常见体验问题:每次进页面都要重新查一遍(慢、费 credits)、刷新后历史没了、断网直接白屏。解法是把查询结果存到浏览器本地------IndexedDB。这篇用极简封装(idb-keyval)把它做出来:缓存带 TTL、历史列表、容量清理,一个下午就能上线。

为什么不用 localStorage

localStorage IndexedDB
容量 ~5MB 几十 MB 到 GB 级
数据形态 只有字符串 结构化对象(不用 JSON.stringify)
同步/异步 同步(大对象会卡) 异步(不卡渲染)

搜索结果 JSON 动辄几十 KB,一次查 50 个词就超 localStorage 了。数据量一大,IndexedDB 是唯一合理选择。

第一步:装极简封装

IndexedDB 原生 API 又啰嗦又有回调历史包袱,用 idb-keyval:

bash 复制代码
npm install idb-keyval
ts 复制代码
import { get, set, keys, del } from "idb-keyval";

第二步:带 TTL 的查询缓存

ts 复制代码
const TTL = 60 * 60 * 1000;   // 1 小时

async function fetchWithCache(keyword: string, hl: string, gl: string) {
  const cacheKey = `serp:${hl}:${gl}:${keyword}`;
  const cached = await get<{ data: SerpResult; savedAt: number }>(cacheKey);

  // 缓存未过期直接用
  if (cached && Date.now() - cached.savedAt < TTL) {
    return cached.data;
  }

  const data = await fetchSerp(keyword, hl, gl);
  await set(cacheKey, { data, savedAt: Date.now() });
  return data;
}

三个设计点 :缓存键含全部参数(hl/gl 都要进键,否则串市场)、TTL 按数据新鲜度定(排名小时级、新闻分钟级)、savedAt 自己带(别依赖系统时间做业务逻辑------但 TTL 判断够用)。

第三步:查询历史列表

ts 复制代码
// 最近查过的词(倒序展示)
async function history() {
  const allKeys = (await keys()).filter((k) => String(k).startsWith("serp:"));
  const items = await Promise.all(
    allKeys.map(async (k) => ({ key: k, val: await get<{ savedAt: number }>(k) }))
  );
  return items.sort((a, b) => (b.val?.savedAt ?? 0) - (a.val?.savedAt ?? 0));
}

历史的价值不只是「回看」------用户切回上一个查过的词时,直接命中缓存,零请求、零等待。

第四步:容量清理(别把用户磁盘撑爆)

IndexedDB 不是无限的。两个策略:

策略一:只存投影字段。 别把整个原始响应塞进去------存你真正用的字段:

ts 复制代码
await set(key, {
  savedAt: Date.now(),
  data: {
    organic: data.organic.slice(0, 10).map((r) => ({
      rank: r.rank,
      title: r.title,
      link: r.link,
    })),
  },
});

策略二:条数上限 + 最旧淘汰(LRU 简化版):

ts 复制代码
async function prune(maxItems = 200) {
  const list = await history();
  if (list.length <= maxItems) return;
  for (const item of list.slice(maxItems)) {
    await del(item.key);   // 删最旧的
  }
}

查询后调用 prune(),用户无感知。

第五步:异常降级

无痕模式、隐私模式、部分浏览器配置下 IndexedDB 可能不可用。包一层降级:

ts 复制代码
let useIDB = true;
try {
  await set("__ping", 1);
} catch {
  useIDB = false;   // 降级:用内存 Map(会话内有效)
}
const memoryCache = new Map();

降级到内存缓存:至少当前会话内快,不报错。

踩坑记录

坑 1:直接存整个原始响应。 单次几十 KB、几百次就是几十 MB,用户的磁盘配额无底洞。存投影字段。

坑 2:用 localStorage 硬塞。 5MB 上限 + 同步 API 卡主线程。结构化存储用 IDB。

坑 3:缓存没有 TTL。 用户三天后进来,看到三天前的「当前排名」。TTL 按场景定,过期必须重新查。

坑 4:忘记异步。 const v = get(key) 拿到的是 Promise 不是值。IDB 全异步,await 写全。

坑 5:无痕模式直接崩。 IndexedDB 在某些模式下抛异常。try/catch 降级到内存。

坑 6:没有清理策略。 历史无限增长,直到浏览器逐出整个站点的存储(更糟)。条数上限 + 最旧淘汰。

工程清单

  1. 数据量大选 IndexedDB,别用 localStorage
  2. 缓存键含全部参数(关键词 + 市场 + 语言)
  3. TTL 按新鲜度定,过期重查
  4. 只存投影字段,别存整个原始响应
  5. 条数上限 + LRU 淘汰 + 异常降级内存缓存
  6. 查询历史列表命中缓存时零请求

IndexedDB 是搜索工具「从一次性网页变成顺手工具」的关键一环:第二次打开同一个词,就该是瞬时的。配合请求层缓存(SWR 式)和 PWA(上期讲过),本地体验就完整了。

接口字段(organic、rank 等)说明见 SerpBase 官方文档,做投影时对照文档挑字段。你们的前端缓存用的是什么方案?评论区聊聊。

相关推荐
kaixin_啊啊6 小时前
【零基础学AI】第 19 章——用分类模型识别类别
人工智能·分类·数据挖掘
隔窗听雨眠9 小时前
从取数到行动:数据分析失效的链路拆解与闭环重建
大数据·数据挖掘·数据分析
四季豆331 天前
AI时代中小企业如何低成本提升数据和知识质量?
大数据·人工智能·数据挖掘
admin and root1 天前
《神农安全.AI漏洞挖掘src赏金》单独开放!
安全·web安全·网络安全·数据挖掘·渗透测试·ai安全·白帽子黑客
数据皮皮侠2 天前
全国休闲农业与乡村旅游示范县政策DID(2010-2025)
大数据·人工智能·搜索引擎·数据挖掘·智慧城市·制造
MindUp2 天前
AI 能帮我做数据分析吗?从使用场景看三类辅助方式
人工智能·数据挖掘·数据分析
听风吹等浪起3 天前
第25章:基于ConvNeXt实现肺部CT肺癌四分类:数据集、训练调优、网络结构与Streamlit网页部署
人工智能·分类·数据挖掘
总要学点什么3 天前
第79篇:数据可视化-模拟迁徙效果——把三十条航线画成会飞的弧
3d·信息可视化·数据挖掘·数据分析·gis·webgl·cesium
Apok'3 天前
零基础学Python量化(1):研究大纲与流程
python·机器学习·金融·数据挖掘·数据分析
STLearner3 天前
KDD 2026 | (2月轮)时空数据(Spatial-Temporal)论文总结时空【下】(人群移动,城市科学(资源调度,)
大数据·人工智能·python·深度学习·机器学习·语言模型·数据挖掘