Use Cases (更新: 2026/6/2)

用Claude Code开发Chrome扩展:Manifest V3、消息通信与最小权限

用Claude Code构建Chrome MV3扩展,覆盖manifest、service worker、content script、权限、存储与测试。

用Claude Code开发Chrome扩展:Manifest V3、消息通信与最小权限

先确定边界,再让Claude Code写代码

Chrome扩展看起来只是几个小文件,但实际包含Manifest V3、后台service worker、content script、消息通信、权限申请、存储、安全审查、打包和商店说明。如果只对Claude Code说“帮我做一个Chrome扩展”,它很容易生成一个过大的脚手架:<all_urls>tabs、popup、React、Vite、外部脚本和难以解释的权限都会被混在一起。

这篇文章用一个很小的例子说明正确做法:在https://example.com/*页面中,用户选中文字后,通过右键菜单把选中的词高亮。我们只使用manifest.jsonservice-worker.jscontent-script.jschrome.storage.local和runtime message。这样做的好处是范围清楚、代码可复制、权限容易解释,也更适合交给Claude Code反复审查。

先解释几个术语。Manifest V3是Chrome扩展当前主要的清单格式,用来声明扩展名称、版本、权限和入口脚本。service worker是事件驱动的后台处理器,不会像旧background page那样一直常驻。content script是注入到网页里的脚本,可以读取和修改DOM。message passing是service worker和content script之间发送JSON消息的方式。若在代理开发里看到harness,可以理解为“让代理安全工作的脚手架”。

开发时要始终对照官方文档。Manifest格式见Chrome Extensions Manifest,service worker见About extension service workers,content script见Chrome Content scriptsMDN Content scripts,权限设计见Chrome Declare permissionsMDN permissions。如果想改进提示词,可以同时阅读站内的Claude Code生产力技巧

三个实际使用场景

第一个场景是团队文档审阅。比如扩展只允许在https://docs.example.com/*运行,审阅者选中一个产品名后,右键高亮页面里的相关词。由于匹配域名很窄,安全说明也很简单:扩展只会处理内部文档站点。

第二个场景是客服或运营后台辅助。客服人员在订单页面选中订单号,右键后复制、标记或高亮相关信息。这个场景会碰到个人信息,因此要让Claude Code列出“读取什么、保存什么、发送什么、不读取什么”。不要把便利性放在数据边界之前。

第三个场景是内容和SEO检查。content script可以在草稿页面检查description长度、h2数量、内部链接和官方外部链接。它可以配合Claude Code CLI工具开发里的命令行检查,也可以参考Claude Code安全最佳实践建立发布前审查流程。

给Claude Code的提示词

MV3扩展的提示词要同时包含功能、禁止事项和验证方式。尤其要写明service worker不是常驻进程,content script不能随意访问所有扩展API,权限必须最小化。

请制作一个Manifest V3 Chrome扩展示例。

要求:
- 目标URL限制为 https://example.com/*
- 通过右键菜单高亮选中的文字
- 使用纯JavaScript文件: manifest.json、service-worker.js、content-script.js
- 使用 chrome.storage.local 保存 enabled 和 color
- service worker与content script通过runtime message通信
- 禁止使用 <all_urls>、tabs、eval、外部CDN脚本或远程代码
- 添加一个Playwright smoke脚本,确认扩展已被加载
- 根据Chrome官方文档写出权限审查表

这个提示词的重点是“不要扩展范围”。Claude Code很可能主动添加popup、options page、图标、打包器和更多权限。第一次实现时,应先把权限、消息通信、DOM操作和测试做小,再决定是否增加UI。

文件结构与Manifest

目录保持极简,方便在chrome://extensions中直接使用“Load unpacked”加载。

mv3-highlighter/
  manifest.json
  service-worker.js
  content-script.js
  package.json
  playwright-extension-smoke.mjs

manifest.json声明扩展的入口、权限和目标页面。这里不使用host_permissions,而是在content_scripts.matches中把页面限制到https://example.com/*

{
  "manifest_version": 3,
  "name": "Claude Code MV3 Highlighter",
  "version": "0.1.0",
  "description": "Highlights selected text on example.com from a context menu.",
  "action": {
    "default_title": "MV3 Highlighter"
  },
  "permissions": ["contextMenus", "storage"],
  "background": {
    "service_worker": "service-worker.js",
    "type": "module"
  },
  "content_scripts": [
    {
      "matches": ["https://example.com/*"],
      "js": ["content-script.js"],
      "run_at": "document_idle"
    }
  ]
}

权限审查可以写成表格,先说明为什么需要,再说明刻意不使用什么。

项目本例设置理由不使用
API权限contextMenus, storage右键菜单和设置保存tabs, scripting, downloads
页面范围https://example.com/*只处理一个演示域名<all_urls>
host permissions静态content script匹配已足够宽泛主机权限
远程代码降低MV3和商店审查风险CDN脚本、eval

service worker:接收事件并发送消息

MV3的service worker会在事件到来时启动,处理结束后可能停止。因此不要把重要状态只放在内存变量中。设置值应该保存在chrome.storage.local,需要时再读取。

const MENU_ID = "highlight-selection";
const DEFAULT_SETTINGS = {
  enabled: true,
  color: "#fff176"
};

async function readSettings() {
  return chrome.storage.local.get(DEFAULT_SETTINGS);
}

chrome.runtime.onInstalled.addListener(async () => {
  const settings = await readSettings();
  await chrome.storage.local.set(settings);

  chrome.contextMenus.create({
    id: MENU_ID,
    title: "Highlight selected text",
    contexts: ["selection"],
    documentUrlPatterns: ["https://example.com/*"]
  });
});

chrome.contextMenus.onClicked.addListener(async (info, tab) => {
  if (info.menuItemId !== MENU_ID || !tab?.id || !info.selectionText) {
    return;
  }

  const settings = await readSettings();
  await chrome.tabs.sendMessage(tab.id, {
    type: "HIGHLIGHT_SELECTION",
    text: info.selectionText.slice(0, 120),
    enabled: settings.enabled,
    color: settings.color
  });
});

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type === "GET_SETTINGS") {
    readSettings().then(sendResponse);
    return true;
  }

  if (message?.type === "SAVE_SETTINGS") {
    const nextSettings = {
      enabled: Boolean(message.enabled),
      color: String(message.color || DEFAULT_SETTINGS.color)
    };
    chrome.storage.local.set(nextSettings).then(() => {
      sendResponse({ ok: true, settings: nextSettings });
    });
    return true;
  }

  return false;
});

这里最容易出错的是异步sendResponse。如果在Promise结束后才调用sendResponse,监听器必须return true,否则消息通道可能提前关闭。审查时要对照Chrome Message passing

content script:安全地修改DOM

content script可以操作页面DOM,但不应该承担所有业务逻辑,也不要放API密钥或Claude令牌。它接收service worker发来的消息,然后只做必要的页面修改。下面的代码还写入一个data-*标记,方便Playwright确认脚本已注入。

const HIGHLIGHT_CLASS = "cc-mv3-highlight";

document.documentElement.dataset.ccMv3Highlighter = "ready";

function shouldSkipNode(node) {
  const parent = node.parentElement;
  if (!parent) return true;
  return Boolean(
    parent.closest(
      `script, style, textarea, input, [contenteditable="true"], .${HIGHLIGHT_CLASS}`
    )
  );
}

function clearHighlights() {
  for (const mark of document.querySelectorAll(`.${HIGHLIGHT_CLASS}`)) {
    const text = document.createTextNode(mark.textContent || "");
    mark.replaceWith(text);
    text.parentElement?.normalize();
  }
}

function createMark(text, color) {
  const mark = document.createElement("mark");
  mark.className = HIGHLIGHT_CLASS;
  mark.textContent = text;
  mark.style.backgroundColor = color;
  mark.style.color = "#111";
  mark.style.padding = "0 2px";
  return mark;
}

function highlightText(term, color) {
  const query = term.trim();
  if (!query) return 0;

  clearHighlights();

  const walker = document.createTreeWalker(document.body, NodeFilter.SHOW_TEXT, {
    acceptNode(node) {
      if (shouldSkipNode(node)) return NodeFilter.FILTER_REJECT;
      return node.nodeValue.toLowerCase().includes(query.toLowerCase())
        ? NodeFilter.FILTER_ACCEPT
        : NodeFilter.FILTER_SKIP;
    }
  });

  const matches = [];
  while (walker.nextNode()) {
    const node = walker.currentNode;
    const index = node.nodeValue.toLowerCase().indexOf(query.toLowerCase());
    if (index >= 0) matches.push({ node, index });
  }

  for (const { node, index } of matches.reverse()) {
    const selected = node.splitText(index);
    selected.splitText(query.length);
    selected.replaceWith(createMark(selected.nodeValue, color));
  }

  return matches.length;
}

chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
  if (message?.type !== "HIGHLIGHT_SELECTION") {
    return false;
  }

  if (!message.enabled) {
    clearHighlights();
    sendResponse({ ok: true, count: 0 });
    return false;
  }

  const count = highlightText(String(message.text || ""), message.color || "#fff176");
  sendResponse({ ok: true, count });
  return false;
});

这个高亮器不是完整的富文本引擎。它只处理每个text node中的第一个匹配项,并跳过表单、脚本、样式和可编辑区域。上线前还要测试多次匹配、Shadow DOM、动态加载内容、已有mark元素以及SPA页面。

Playwright和手动测试

扩展的完整自动化测试不容易,但smoke test至少能确认Chrome加载了扩展,并且content script进入目标页面。Playwright可使用launchPersistentContext加载未打包扩展。

{
  "type": "module",
  "scripts": {
    "smoke": "node playwright-extension-smoke.mjs"
  },
  "devDependencies": {
    "playwright": "^1.52.0"
  }
}
import { chromium } from "playwright";
import path from "node:path";
import { fileURLToPath } from "node:url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const extensionPath = path.resolve(__dirname);
const userDataDir = path.resolve(__dirname, ".pw-extension-profile");

const context = await chromium.launchPersistentContext(userDataDir, {
  headless: false,
  args: [
    `--disable-extensions-except=${extensionPath}`,
    `--load-extension=${extensionPath}`
  ]
});

const page = await context.newPage();
await page.goto("https://example.com/");
await page.waitForFunction(() => {
  return document.documentElement.dataset.ccMv3Highlighter === "ready";
});

console.log("Extension content script is loaded on https://example.com/");
await page.waitForTimeout(3000);
await context.close();
npm install
npm run smoke

手动测试也要保留:打开chrome://extensions,启用Developer mode,选择Load unpacked,加载目录。然后打开https://example.com/,选择标题文字,右键点击“Highlight selected text”。如果出现高亮,service worker控制台没有错误,第一轮就通过。

常见失败与发布前检查

第一个失败是过早加入tabs权限。本例只需要从context menu事件拿到tab.id并发送消息,不需要读取所有标签页信息。第二个失败是把秘密信息放在content script中。它靠近页面内容,不适合保存API密钥、Claude令牌或客户数据。第三个失败是误以为service worker会一直活着,导致内存缓存丢失。第四个失败是用innerHTML粗暴替换DOM,破坏事件和可访问性。第五个失败是在调试阶段用了<all_urls>,打包前忘记收窄。

发布前按Prepare your Extension准备图标、截图、隐私说明、权限说明和简短描述。打包时不要把node_modules和Playwright profile放进去。

zip -r mv3-highlighter.zip manifest.json service-worker.js content-script.js
Compress-Archive -Path manifest.json,service-worker.js,content-script.js -DestinationPath mv3-highlighter.zip -Force

变现CTA与实际验证记录

Chrome扩展开发的价值不只是生成代码,而是把权限、安全边界、测试和商店说明整理成可复用流程。ClaudeCodeLab可以通过Claude Code培训与导入咨询帮助团队审查MV3设计、权限说明、测试清单和Claude Code提示词。想自学模板和清单的读者,也可以查看教材列表

Masa按这个模式试做时,最先暴露的问题不是JavaScript语法,而是权限解释。使用<all_urls>当然能更快演示,但很难向用户说明为什么需要访问所有网站。改成https://example.com/*并只保留contextMenusstorage后,Chrome扩展管理页面的警告更少,Claude Code给出的安全审查意见也更具体。MV3扩展的目标不只是“能运行”,而是“用可解释的权限运行”。

#Claude Code #browser extension #Chrome extension #Manifest V3 #JavaScript
免费

免费 PDF: Claude Code 速查表

输入邮箱即可获取一页 PDF,整理常用命令、审查习惯和安全工作流。

我们会妥善保护你的信息,不发送垃圾邮件。

把 Claude Code 变成真正能带来结果的工作流

先领取中文说明的免费 PDF,再进入英文商品页选择合适的教材。如果你需要团队落地、流程设计或内容变现支持,也可以直接咨询。

Masa

关于作者

Masa

专注 Claude Code 实务流程、团队导入和内容转化的工程师。