Skip to content

怎么写一个显示 IP 归属地的浏览器插件?Manifest V3 实战(JavaScript) ​

排查线上问题的时候,经常要跟 IP 打交道:日志里一个来源 IP 飘过来,想立刻知道它是哪个省、哪家运营商;本地出口 IP 换没换,也得开网页查。每次都要复制、开新标签、粘进查询站,一天重复十几次。Chrome 商店里其实有不少现成的 IP 查询插件,但要么带广告,要么数据源不透明,要么权限要一大堆。干脆自己写一个:Manifest V3 扩展,右键选中 IP 一键查归属地,点图标还能看本机出口 IP,全本地代码、无广告、数据来自自己指定的接口。

先说明一个边界:浏览器插件拿不到目标网站的服务器 IP(浏览器安全模型不允许扩展做 DNS 解析),能可靠拿到的是「选中文本里的 IP」和「本机出口 IP」。这篇就实现这两个能力,不写做不到的功能。

一、插件要做什么 ​

  1. 本机出口 IP 查询:点工具栏图标,弹出面板,显示当前出口 IP 的归属地(国家/省市/运营商);
  2. 选中文本查 IP:网页里选中一个 IP,右键菜单点「查询 IP 归属地」,系统通知直接弹结果;
  3. 批量查询:弹出面板里粘一堆 IP,一次查完列表展示。

三种功能的数据都来自 https://ip9.com.cn/get?ip=xxx(不传参返回当前出口 IP),免费版 60 次/分钟对个人使用绰绰有余。

二、Manifest V3 项目结构 ​

一个扩展 = 一个文件夹,四个文件:

ip-lookup-extension/
├── manifest.json      # 扩展清单
├── background.js      # 后台 Service Worker:右键菜单 + 通知
├── popup.html         # 点图标的弹出面板
└── popup.js           # 面板逻辑

manifest.json ​

json
{
  "manifest_version": 3,
  "name": "IP 归属地速查",
  "version": "1.0.0",
  "description": "选中页面里的 IP 一键查归属地,查看本机出口 IP",
  "permissions": ["contextMenus", "notifications", "storage"],
  "host_permissions": ["https://ip9.com.cn/*"],
  "background": { "service_worker": "background.js" },
  "action": {
    "default_popup": "popup.html",
    "default_title": "IP 归属地速查"
  }
}

host_permissions 只放了 ip9.com.cn,这是 MV3 的规范做法:权限最小化,不给「任意网站」权限,商店审核更容易过。

background.js(右键菜单 + 通知) ​

javascript
const API = "https://ip9.com.cn/get";

async function lookup(ip) {
  const r = await fetch(`${API}?ip=${encodeURIComponent(ip)}`, { cache: "no-store" });
  const j = await r.json();
  if (j.ret !== 200) throw new Error("查询失败: " + ip);
  return j.data;
}

chrome.runtime.onInstalled.addListener(() => {
  chrome.contextMenus.create({
    id: "lookup-ip",
    title: "查询 IP 归属地",
    contexts: ["selection"]
  });
});

chrome.contextMenus.onClicked.addListener(async (info) => {
  const ip = (info.selectionText || "").trim();
  if (!/^\d{1,3}(\.\d{1,3}){3}$/.test(ip)) return;   // 只处理 IPv4 文本
  try {
    const d = await lookup(ip);
    chrome.notifications.create({
      type: "basic",
      iconUrl: "icons/icon128.png",
      title: d.ip + " 归属地",
      message: `${d.country} ${d.prov} ${d.city}\n运营商:${d.isp || "未知"}`
    });
    chrome.storage.session.set({ lastResult: d });   // 供面板展示
  } catch (e) {
    chrome.notifications.create({
      type: "basic", iconUrl: "icons/icon128.png",
      title: "查询失败", message: String(e)
    });
  }
});

MV3 的 Service Worker 不是常驻的,所以不缓存任何内存状态,查询结果放 chrome.storage.session,面板打开时再读。

html
<!DOCTYPE html>
<html lang="zh-CN">
<head>
  <meta charset="utf-8">
  <style>
    body { width: 320px; font: 13px/1.6 sans-serif; margin: 12px; }
    #out { background: #f6f8fa; border-radius: 6px; padding: 8px 12px; margin-bottom: 10px; }
    textarea { width: 100%; height: 60px; box-sizing: border-box; }
    button { margin-top: 6px; width: 100%; padding: 5px; }
  </style>
</head>
<body>
  <div id="out">正在查询本机出口 IP…</div>
  <textarea id="ips" placeholder="每行一个 IP,支持批量查询"></textarea>
  <button id="go">批量查询</button>
  <div id="list"></div>
  <script src="popup.js"></script>
</body>
</html>
javascript
const API = "https://ip9.com.cn/get";

// 打开面板即查本机出口 IP(不传参数)
fetch(API, { cache: "no-store" })
  .then(r => r.json())
  .then(j => {
    const d = j.data;
    document.getElementById("out").textContent =
      `出口 IP:${d.ip}\n归属地:${d.country} ${d.prov} ${d.city} 运营商:${d.isp}`;
  });

// 批量查询:逐行查,结果追加到列表
document.getElementById("go").addEventListener("click", async () => {
  const list = document.getElementById("list");
  list.innerHTML = "";
  const ips = document.getElementById("ips").value
    .trim().split(/\s+/).filter(s => /^\d{1,3}(\.\d{1,3}){3}$/.test(s));
  for (const ip of ips) {
    const row = document.createElement("div");
    try {
      const j = await (await fetch(`${API}?ip=${ip}`, { cache: "no-store" })).json();
      const d = j.data;
      row.textContent = `${d.ip} — ${d.country} ${d.prov} ${d.city}(${d.isp})`;
    } catch (e) {
      row.textContent = `${ip} — 查询失败`;
    }
    list.appendChild(row);
    await new Promise(s => setTimeout(s, 1100));   // 限速 1.1s,配合 60 次/分钟额度
  }
});

注意两处 MV3 硬性要求:html 里不能有内联 script(所以单独 popup.js),不能引用任何远程 JS(代码全本地),不然扩展无法加载。

三、加载与发布 ​

  • 本地调试:Chrome 打开 chrome://extensions → 打开「开发者模式」→「加载已解压的扩展程序」选文件夹。图标文件(icon16/48/128.png)没有的话,先用任意 128px PNG 顶替,或直接在 manifest 里删掉 icons 字段也能加载;
  • 测试:随便打开一个页面,选中文中的 IP → 右键 → 「查询 IP 归属地」;点扩展图标看出口 IP、粘一批 IP 批量查。免费接口 60 次/分钟,批量查询脚本已限速 1.1 秒/次;
  • 上架商店:Chrome Web Store 发布要过审核,重点准备两项——manifest 里声明 host_permissions 的原因(只查接口、不读页面数据),以及隐私政策页(说明只把 IP 发给查询接口、不留存)。权限越少越容易过。

四、落地注意事项 ​

  • 只能查 IPv4 文本:上面的正则只匹配点分十进制 IPv4;要支持 IPv6 得加一段正则(接口本身支持 IPv6),这里不展开;
  • 别在插件里做「页面所有 IP」:扩展没有 DNS 解析能力,想显示「当前网站的服务器 IP」做不到,别被这类需求带偏,做到选中即查就已经覆盖绝大多数用法;
  • 缓存结果:同一 IP 高频查看时,先查 storage 再查接口,能省额度也更快;批量的限速不能省,不然会被接口限流(ret=429);
  • 数据源可换:API 地址在 background.js 和 popup.js 顶部各有一处常量,想换别的服务商改一处字符串即可。

总结 ​

一个 MV3 扩展总共四个文件、一百多行代码,权限最小化、数据源可控、没广告,点图标查出口 IP、选中 IP 右键即查、批量粘贴一起查,日常排查够用了。调试流程:chrome://extensions 加载已解压文件夹 → 右键验证 → 满意了再整理图标和隐私政策上架。查询接口直接用免费版 https://ip9.com.cn/get?ip=xxx(不传参查本机出口 IP,60 次/分钟,个人插件完全够用),要机房/家庭 IP 类型判断再考虑 VIP。官网:https://www.ip9.com.cn