搜索框的联想/自动补全,大部分实现用的是「内置词库」或「热门词表」。但用户真正在搜什么,答案在谷歌的相关搜索里------related_searches 和 people_also_ask 就是现成的真实联想词。这篇记录我做一个基于搜索数据 API 的联想组件:防抖、缓存、可复用,前端向,代码可直接跑。
需求
用户在搜索框打字时,出现基于真实搜索数据的联想词。要求:
- 输入停顿后请求(防抖),不每键都发
- 相同输入不重复请求(前端缓存)
- 做成可复用组件,不绑定业务
数据源
联想词来自搜索数据 API 的返回。我用的是 SerpBase(serpbase.dev)的 /google/search 端点,返回里带 related_searches(相关搜索)和 people_also_ask(人们还问),POST JSON + X-API-Key 认证。
关键安全点:API key 不能放浏览器。联想请求必须走后端代理,前端只调自己的接口。
第一步:后端代理接口
一个极薄的代理,把联想词捞出来(Node 版):
js
// server.mjs
import express from "express";
const app = express();
const SERPBASE = "https://api.serpbase.dev";
const API_KEY = process.env.SERPBASE_API_KEY; // key 只在服务端
app.get("/api/suggest", async (req, res) => {
const q = (req.query.q || "").trim();
if (!q) return res.json([]);
const resp = await fetch(`${SERPBASE}/google/search`, {
method: "POST",
headers: { "Content-Type": "application/json", "X-API-Key": API_KEY },
body: JSON.stringify({ q, hl: "zh-cn", gl: "cn" }),
});
const data = await resp.json();
// 把相关搜索 + PAA 问题合并成联想词列表
const related = data.related_searches ?? [];
const paas = (data.people_also_ask ?? []).map((p) => p.question);
res.json([...new Set([...related, ...paas])].slice(0, 8));
});
app.use(express.static("public"));
app.listen(3000, () => console.log("http://localhost:3000"));
第二步:前端联想组件
做成一个可复用的 Web Component,不依赖框架:
html
<!-- public/index.html -->
<script>
class KeywordSuggestion extends HTMLElement {
connectedCallback() {
this._cache = new Map(); // 请求缓存:相同输入不重复发
this._timer = null;
this.innerHTML = `
<input placeholder="输入关键词,看真实联想词..." />
<ul hidden></ul>`;
const input = this.querySelector("input");
const list = this.querySelector("ul");
input.addEventListener("input", (e) => {
clearTimeout(this._timer);
const q = e.target.value.trim();
if (!q) { list.hidden = true; return; }
this._timer = setTimeout(() => this._suggest(q, list), 300); // 防抖 300ms
});
}
async _suggest(q, list) {
if (this._cache.has(q)) {
this._render(this._cache.get(q), list); // 命中缓存,不发请求
return;
}
const words = await fetch(`/api/suggest?q=${encodeURIComponent(q)}`).then(r => r.json());
this._cache.set(q, words);
this._render(words, list);
}
_render(words, list) {
list.innerHTML = words.map(w => `<li data-word="${w}">${w}</li>`).join("");
list.hidden = words.length === 0;
list.querySelectorAll("li").forEach(li =>
li.addEventListener("click", () => {
this.querySelector("input").value = li.dataset.word;
list.hidden = true;
})
);
}
}
customElements.define("keyword-suggestion", KeywordSuggestion);
</script>
<keyword-suggestion></keyword-suggestion>
要点:
- 防抖 300ms:停止输入 300ms 后才请求,避免每键一请求
- 前端缓存 Map:相同输入直接命中,不重复调接口、不浪费 credits
- Web Component :
<keyword-suggestion>一处定义,多处复用
第三步:效果与体验优化
几个值得加的点:
空态提示。 如果联想词为空,提示「无联想词」而不是空白,体验更好。
点击联想后继续联想。 点击一个联想词,可以把它当作新输入再触发联想------形成「词 → 相关词」的探索链:
js
// 点击后把词填入输入框,再自动触发一次联想
li.addEventListener("click", () => {
input.value = li.dataset.word;
setTimeout(() => this._suggest(input.value, list), 100);
});
选中高亮。 键盘上下键导航是加分项,这里先不展开。
成本和注意点
/google/search每次 1 credits;前端缓存保证了「同一用户同一次输入只请求一次」- 注册送 100 次免费搜索,开发测试完全够
- 标准包 10买2万次(0.50/千次),永不过期------这种交互型调用量不大
related_searches/people_also_ask是可选字段,服务端用?? []兜底
收尾
这个组件的价值,是把「谷歌的真实搜索数据」变成产品里的交互能力------搜索框联想、选题推荐、站内搜索补全都能用。核心技术点就三个:防抖、缓存、后端代理(保 key 安全)。
接口参数细节在 SerpBase 官方文档。从「复制组件 + 一个代理接口」开始,你就能在任意页面拥有基于真实搜索数据的联想功能。有更好的联想交互,评论区聊聊。