写个浏览器插件查 SERP:选中关键词右键看前 10(Chrome MV3 实战)

做 SEO 的朋友提过一个需求:写文章时在页面上看到个词,想立刻知道它的搜索结果长什么样------现在的流程是复制 → 打开新标签 → 粘贴 → 回车,一天重复几十次。于是我用一个周末写了个浏览器插件:选中文字,右键「查看搜索结果」,弹窗里直接看前 10。这篇把这个插件(Chrome MV3)的实现完整讲一遍,包括最关键的 Key 存储问题。

插件长什么样

三个组件:

  1. 右键菜单:选中文字后出现「查看『xx』的搜索结果」
  2. 后台脚本(service worker):调搜索接口,把结果暂存
  3. 结果弹窗:显示前 10 名、耗时和扣费

第一步:manifest.json

json 复制代码
{
  "manifest_version": 3,
  "name": "SERP Peek",
  "version": "1.0.0",
  "description": "选中关键词,右键查看搜索结果前 10",
  "permissions": ["contextMenus", "storage"],
  "host_permissions": ["https://api.serpbase.dev/*"],
  "background": { "service_worker": "background.js" },
  "options_page": "options.html"
}

两个容易漏的配置:contextMenus 权限 (右键菜单)和 host_permissions(不声明 API 域名,service worker 里的 fetch 会被拦)。

第二步:右键菜单 + 调接口(background.js)

js 复制代码
chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "serp-peek",
    title: "查看「%s」的搜索结果",
    contexts: ["selection"],
  });
});

let busy = false;   // 防连点:正在查询时忽略新请求

chrome.contextMenus.onClicked.addListener(async (info) => {
  if (busy || !info.selectionText) return;
  busy = true;
  try {
    const { serpbaseKey } = await chrome.storage.local.get("serpbaseKey");
    if (!serpbaseKey) {
      chrome.runtime.openOptionsPage();   // 没配 Key:打开设置页
      return;
    }

    const resp = await fetch("https://api.serpbase.dev/google/search", {
      method: "POST",
      headers: { "Content-Type": "application/json", "X-API-Key": serpbaseKey },
      body: JSON.stringify({ q: info.selectionText, hl: "zh-cn", gl: "cn" }),
    });
    const data = await resp.json();

    // service worker 随时会被回收:结果必须存 chrome.storage
    await chrome.storage.session.set({ lastResult: data, lastQuery: info.selectionText });
    await chrome.windows.create({ url: "result.html", type: "popup", width: 480, height: 640 });
  } finally {
    busy = false;
  }
});

注意到 busy 锁和 chrome.storage.session 没有------这是 MV3 和普通网页开发最大的不同:service worker 会休眠,别用内存变量存任何要用的状态。

第三步:结果弹窗(result.html 的脚本)

js 复制代码
const { lastResult, lastQuery } = await chrome.storage.session.get(["lastResult", "lastQuery"]);
document.querySelector("#query").textContent = lastQuery;

const list = document.querySelector("#list");
for (const r of (lastResult?.organic ?? []).slice(0, 10)) {
  const li = document.createElement("li");
  li.innerHTML = `<span class="rank">${r.rank}</span>
                  <a href="${r.link}" target="_blank">${r.title}</a>`;
  list.appendChild(li);
}

document.querySelector("#meta").textContent =
  `耗时 ${lastResult?.elapsed_ms ?? "-"} ms · 扣费 ${lastResult?.credits_charged ?? "-"} credits`;

把 elapsed_ms 和 credits_charged 显示出来,是给自己看的:每次右键花了多少,心里有数。

第四步:设置页与 Key 存储(最重要的设计决策)

设置页就是一个输入框,把 Key 存进 chrome.storage.local:

js 复制代码
document.querySelector("#save").onclick = async () => {
  const key = document.querySelector("#key").value.trim();
  await chrome.storage.local.set({ serpbaseKey: key });
  document.querySelector("#hint").textContent = "已保存到本机浏览器";
};

这里有个必须想清楚的模式选择:

模式 做法 适合
个人工具 用户自己填 Key,存本地 自用/给同事用;Key 存本机浏览器
对外发布 插件调你自己的后端代理 发布给陌生用户;Key 只在你的服务器

发布版绝对不能把 Key 打进插件包里------任何人解压 crx 都能看到,等于把余额送人。个人工具模式也要提醒:Key 是明文存在浏览器配置里,公用电脑别用。

踩坑记录

坑 1:用内存变量存状态。 service worker 几分钟不活动就被回收,变量全没,弹窗打开是空的。状态一律 chrome.storage。

坑 2:忘声明 host_permissions。 fetch 直接失败,控制台报权限相关的错。API 域名要写进 manifest。

坑 3:Key 打进包里发布。 这是最严重的一条------个人自用可以存本地,发布必须走后端代理。

坑 4:右键菜单 title 没写 %s。 菜单显示的是死文案,带不出选中的词。

坑 5:没有防连点。 右键狂点会连发请求,每次都扣 credits。加个 busy 锁。

坑 6:在 service worker 里用 XMLHttpRequest。 MV3 里用 fetch,XHR 已经不合适了。

工程清单

  1. manifest 声明 contextMenus + host_permissions
  2. 状态全部进 chrome.storage(session/local 按用途分)
  3. 个人工具:Key 存本地 + 风险提示;对外发布:一律走代理
  4. 右键菜单 title 用 %s 带选中文字
  5. 防连点锁,避免误扣 credits
  6. 结果页展示 elapsed_ms / credits_charged,成本心中有数

浏览器插件算是「搜索数据的最近触点」------不用切窗口、不用复制粘贴,选中即查。代码量一个周末足够,但有两条线要先想清楚:Key 存哪(个人 vs 发布) 、状态放哪(storage,不是内存)。跨过这两条,剩下的就是普通的 Web 开发。

接口的鉴权头和各字段说明在 SerpBase 官方文档。你平时最想把哪些搜索数据操作「浓缩」到浏览器里?评论区聊聊。

相关推荐
Flynt1 小时前
"MySQL搜不动就上ES"?我先在50万行数据上测了它自带的ngram全文索引
mysql·elasticsearch·搜索引擎
用户7783366132111 小时前
把地图数据画出来:Leaflet + 搜索 API 做商家分布图
api
超兔一体云1 小时前
Vue3组合式API实战——从Options API迁移
api
用户298698530141 小时前
Python 文档格式转换实战:RTF 转 PDF 与 HTML
python·html·api
用户7783366132112 小时前
前端开发别拿生产 Key 刷数据:用 MSW 给搜索接口做 Mock
前端·api
用户341852229222 小时前
你买的中转站真的是 GPT-6 吗?我写了个不问“你是什么模型”的检测工具
api
LaughingZhu5 天前
Product Hunt 每日热榜 | 2026-09-19
人工智能·深度学习·神经网络·搜索引擎·百度
VIP_CQCRE6 天前
Coze 接入自定义模型更简单:用 Ace Data Cloud 打通 OpenAI Responses API
openai·api·ai agent·coze·acedatacloud