前端本地存搜索数据: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 官方文档,做投影时对照文档挑字段。你们的前端缓存用的是什么方案?评论区聊聊。

相关推荐
YangYang9YangYan6 小时前
2027 届校招|大数据管理与应用专业投递供应链管培,数据分析能力考察逻辑拆解
数据挖掘·数据分析
卷毛迷你猪19 小时前
快速实验篇(B16)用户级预测可行性审计(否定性意见)
大数据·hadoop·数据挖掘·聚类
Asa1213820 小时前
NC|核质大DNA病毒门(Nucleocytoviricota)生物地理格局及其与欧洲湖泊真核生物互作研究
数据挖掘
2601_968900771 天前
大模型版本回归评估实战:从能力指标到额度口径的完整链路
android·数据挖掘·回归
sbjdhjd1 天前
云安全 | Docker 容器逃逸复盘(二):2375 未授权接口如何突破容器管理边界
网络安全·docker·云原生·数据挖掘·开源·云计算·云安全
2601_962780911 天前
统计背景求职市场调研岗,岗位拆解与数据分析能力准备
数据挖掘·数据分析
DongQiShanRen2 天前
裁决台账双向互校(上):名册与实物的第一道对账
java·linux·运维·数据库·人工智能·自然语言处理·数据挖掘
β添砖java2 天前
机器学习8:数据挖掘案例-南方电网电力负荷预测 ---完结!
人工智能·机器学习·数据挖掘
计算机毕业设计杰瑞3 天前
【最新原创大数据】基于大数据的酒店数据分析与可视化,附源码_高质量项目_可视化_数据分析_毕设选题推荐_SPark_Hadoop_毕设指导
大数据·信息可视化·数据挖掘·课程设计