按 Enter 键跳转到正文

Chrome 插件开发实战:一键把当前页面存为 Markdown

从一行代码到 Chrome Web Store 上架。本文用一个具体例子走通 Chrome 插件开发完整流程:manifest.json、content script、popup 通信、文件下载、用户偏好存储、上架审核。

为什么要做这个插件

我经常在浏览器里看技术文章。看到好的想存到 Obsidian,复制粘贴格式全乱。打开"另存为"保存下来是带广告、导航栏、侧边栏的 HTML。想找一个干净的 Markdown 版本,要么没有,要么是闭源插件不敢装。

装个现成插件不放心。写个 userscript 分发麻烦。干脆自己写一个。

这是个"改造已有网站"的小工具,Chrome 插件是最合适的形态。如果你还不确定插件是不是解决你问题的最佳方案,可以先看 《Chrome 插件开发》

我们要做的版本

最终交付一个能本地转、能下载、能存用户偏好的插件:

  • 点击工具栏图标,弹出一个 popup
  • popup 上一个按钮"保存为 Markdown"
  • 点击后,把当前页面转成干净的 Markdown,触发下载
  • 几个 checkbox 让用户自定义:是否包含图片、是否包含代码块、文件名格式

这个版本足够简单,又覆盖了插件开发的核心链路:content script、popup 通信、文件下载、用户偏好存储。

准备工作

需要:

  • Chrome 浏览器(任何最近版本都行)
  • 一个空文件夹
  • 一个文本编辑器

不需要:

  • Node.js
  • npm
  • 任何前端框架
  • 任何构建工具

整个项目就是几个静态文件,加一个第三方库。装一个依赖就要搭构建,搭构建就要讲 webpack / vite / TypeScript,整个文章会跑偏。

第三方库:Turndown.js

用来把 HTML 转成 Markdown。同类插件 80% 在用,成熟稳定。

下载地址:https://unpkg.com/turndown@7.2.0/dist/turndown.js

下载下来保存到 vendor/turndown.js(自己创建 vendor 目录)。

项目结构:

 1markdown-saver/
 2├── manifest.json
 3├── content.js
 4├── popup.html
 5├── popup.js
 6├── icons/
 7│   ├── icon16.png
 8│   ├── icon32.png
 9│   ├── icon48.png
10│   └── icon128.png
11└── vendor/
12    └── turndown.js

图标先随便找 4 张占位图(每个尺寸一张),后面会讲商店要求。

打开 chrome://extensions,打开右上角的"开发者模式"。

第一个能跑的插件

先做一个最小可运行的版本。目标:让 Chrome 认识我们的插件,content script 能往控制台打日志。

manifest.json:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "action": {
 7    "default_title": "保存为 Markdown"
 8  },
 9  "content_scripts": [
10    {
11      "matches": ["<all_urls>"],
12      "js": ["content.js"]
13    }
14  ]
15}

content.js:

1console.log("[Markdown Saver] content script loaded");

加载插件:

  1. 打开 chrome://extensions
  2. 点左上角"加载已解压的扩展程序"
  3. 选你的 markdown-saver 文件夹

验证:

打开任意网页,按 F12 打开 DevTools,控制台应该看到 [Markdown Saver] content script loaded

如果没看到:

  • 确认 chrome://extensions 里插件状态是"已启用"
  • 确认 manifest.json 没有语法错误(JSON 必须合法,文件名不能错)
  • 改完文件后必须回到 chrome://extensions 点插件详情页的刷新按钮

到这里,插件已经能跑。剩下的是加功能。

提取内容并转成 Markdown

核心功能:从 content script 里拿到页面的 HTML,用 Turndown 转成 Markdown。

先让 content script 自己做这件事。后面再加 popup 通信。

content.js:

 1// 等待 Turndown 加载
 2window.addEventListener("load", () => {
 3  // 动态注入 Turndown
 4  const script = document.createElement("script");
 5  script.src = chrome.runtime.getURL("vendor/turndown.js");
 6  document.documentElement.appendChild(script);
 7});
 8
 9function initTurndown() {
10  if (typeof TurndownService === "undefined") {
11    setTimeout(initTurndown, 50);
12    return;
13  }
14
15  const turndown = new TurndownService({
16    headingStyle: "atx",
17    codeBlockStyle: "fenced",
18    bulletListMarker: "-",
19    emDelimiter: "*"
20  });
21
22  // 排除导航、页脚、广告等干扰元素
23  turndown.remove([
24    "nav",
25    "aside",
26    "footer",
27    "header",
28    "[role='navigation']",
29    "[role='banner']",
30    ".advertisement",
31    ".ad",
32    ".sidebar",
33    "#sidebar"
34  ]);
35
36  window.__markdownSaver = turndown;
37}

但这段代码有几个问题需要解决。

问题一:Turndown 文件需要被 content script 访问

content script 默认不能直接访问插件目录里的文件。要让 content script 能通过 chrome.runtime.getURL("vendor/turndown.js") 拿到这个文件,必须在 manifest.json 里声明 web_accessible_resources

更新 manifest.json:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "action": {
 7    "default_title": "保存为 Markdown"
 8  },
 9  "content_scripts": [
10    {
11      "matches": ["<all_urls>"],
12      "js": ["content.js"]
13    }
14  ],
15  "web_accessible_resources": [
16    {
17      "resources": ["vendor/turndown.js"],
18      "matches": ["<all_urls>"]
19    }
20  ]
21}

问题二:动态注入的 script 默认运行在"页面世界"

content script 默认运行在"隔离世界",但动态注入的 <script> 标签默认跑在页面的 JavaScript 上下文里。所以我们在 initTurndown 里定义的 window.__markdownSaver 在 content script 自己访问不到。

解决办法:在动态注入时加 document.head 而不是 document.documentElement 没有用,关键是让 content script 通过消息通信让 content script 主动调用。

问题三:content script 和动态注入的脚本之间的通信

最干净的方式是:

  1. content script 注入 Turndown 脚本
  2. content script 通过 window.postMessage 与页面脚本通信
  3. 或者更直接:让 content script 直接执行转 HTML 的动作,但 Turndown 服务在页面里初始化

这里有一个更简洁的方案:把 Turndown 注入到页面世界后,content script 通过自定义事件通信。

content.js:

 1// 注入 Turndown 到页面世界
 2function injectTurndown() {
 3  return new Promise((resolve) => {
 4    const script = document.createElement("script");
 5    script.src = chrome.runtime.getURL("vendor/turndown.js");
 6    script.onload = () => {
 7      // Turndown 加载完后初始化
 8      const initScript = document.createElement("script");
 9      initScript.textContent = `
10        const turndown = new TurndownService({
11          headingStyle: "atx",
12          codeBlockStyle: "fenced",
13          bulletListMarker: "-",
14          emDelimiter: "*"
15        });
16        turndown.remove([
17          "nav", "aside", "footer", "header",
18          "[role='navigation']", "[role='banner']",
19          ".advertisement", ".ad", ".sidebar", "#sidebar"
20        ]);
21        window.__convertToMarkdown = (html) => turndown.turndown(html);
22        window.dispatchEvent(new CustomEvent("turndown-ready"));
23      `;
24      document.documentElement.appendChild(initScript);
25      resolve();
26    };
27    document.documentElement.appendChild(script);
28  });
29}
30
31// 监听转换完成事件
32window.addEventListener("turndown-converted", (e) => {
33  console.log("Converted:", e.detail.markdown);
34});
35
36// 等页面加载完再注入
37if (document.readyState === "complete") {
38  injectTurndown();
39} else {
40  window.addEventListener("load", injectTurndown);
41}

这一段代码看起来绕,原因是要在 content script 和页面世界之间传递 Turndown 这个对象。最简单的方案是干脆让 Turndown 在页面里初始化,content script 通过消息通信调用。

但更简单的方案是:把 Turndown 直接加载到 content script 的隔离世界。

更新一下:content script 加载 Turndown 的方式其实可以更直接。Manifest V3 允许在 content_scripts 里声明多个 JS 文件,依次加载:

更新 manifest.json:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "action": {
 7    "default_title": "保存为 Markdown"
 8  },
 9  "content_scripts": [
10    {
11      "matches": ["<all_urls>"],
12      "js": ["vendor/turndown.js", "content.js"],
13      "run_at": "document_idle"
14    }
15  ]
16}

这样 Turndown 会被 Chrome 自动注入到 content script 的隔离世界,可以直接用。

content.js(简化版):

 1// Turndown 已经被自动注入,直接用
 2const turndown = new TurndownService({
 3  headingStyle: "atx",
 4  codeBlockStyle: "fenced",
 5  bulletListMarker: "-",
 6  emDelimiter: "*"
 7});
 8
 9turndown.remove([
10  "nav",
11  "aside",
12  "footer",
13  "header",
14  "[role='navigation']",
15  "[role='banner']",
16  ".advertisement",
17  ".ad",
18  ".sidebar",
19  "#sidebar"
20]);
21
22// 监听来自 popup 的消息
23chrome.runtime.onMessage.addListener((request, sender, sendResponse) => {
24  if (request.action === "extract") {
25    const html = document.documentElement.outerHTML;
26    const markdown = turndown.turndown(html);
27    sendResponse({ markdown });
28  }
29  return true; // 保留 sendResponse 通道
30});

到这里,content script 已经能做完整的事情:拿 HTML、转 Markdown、返回结果。剩下的只是 popup 怎么调用它。

验证:

现在还没有 popup,先在控制台手动测试:

  1. 重新加载插件(chrome://extensions → 刷新按钮)
  2. 刷新当前网页
  3. F12 DevTools 控制台输入:
1chrome.runtime.sendMessage({ action: "extract" }, (response) => {
2  console.log(response.markdown.slice(0, 200));
3});

应该能看到页面前 200 字符的 Markdown 输出。

让插件"可交互":加 popup

现在加 popup。点击工具栏图标,弹出一个 HTML 页面,里面放一个按钮和几个 checkbox。

popup.html:

 1<!DOCTYPE html>
 2<html>
 3<head>
 4  <meta charset="UTF-8">
 5  <style>
 6    body { width: 280px; padding: 12px; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; }
 7    button { width: 100%; padding: 8px; background: #1a73e8; color: white; border: none; border-radius: 4px; cursor: pointer; }
 8    button:hover { background: #1557b0; }
 9    .option { margin: 8px 0; font-size: 13px; }
10    .status { margin-top: 8px; font-size: 12px; color: #666; }
11  </style>
12</head>
13<body>
14  <button id="save">保存为 Markdown</button>
15  <div class="option">
16    <label><input type="checkbox" id="includeImages" checked> 包含图片</label>
17  </div>
18  <div class="option">
19    <label><input type="checkbox" id="includeCode" checked> 包含代码块</label>
20  </div>
21  <div class="status" id="status"></div>
22  <script src="popup.js"></script>
23</body>
24</html>

更新 manifest.json,加上 popup:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "action": {
 7    "default_popup": "popup.html",
 8    "default_title": "保存为 Markdown"
 9  },
10  "content_scripts": [
11    {
12      "matches": ["<all_urls>"],
13      "js": ["vendor/turndown.js", "content.js"],
14      "run_at": "document_idle"
15    }
16  ]
17}

popup.js:

 1const saveBtn = document.getElementById("save");
 2const statusEl = document.getElementById("status");
 3
 4saveBtn.addEventListener("click", async () => {
 5  saveBtn.disabled = true;
 6  statusEl.textContent = "提取中...";
 7
 8  try {
 9    // 获取当前活动标签页
10    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
11
12    // 向 content script 发消息
13    const response = await chrome.tabs.sendMessage(tab.id, { action: "extract" });
14
15    if (!response || !response.markdown) {
16      statusEl.textContent = "提取失败";
17      return;
18    }
19
20    // 触发下载
21    const title = document.title.replace(/[\\/:*?"<>|]/g, "_");
22    const date = new Date().toISOString().slice(0, 10);
23    const filename = `${title}_${date}.md`;
24
25    await chrome.downloads.download({
26      url: "data:text/markdown;charset=utf-8," + encodeURIComponent(response.markdown),
27      filename: filename,
28      saveAs: true
29    });
30
31    statusEl.textContent = "已保存";
32  } catch (err) {
33    statusEl.textContent = "错误:" + err.message;
34  } finally {
35    saveBtn.disabled = false;
36  }
37});

更新 manifest.json,加上 downloads 权限:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "permissions": ["downloads", "storage"],
 7  "action": {
 8    "default_popup": "popup.html",
 9    "default_title": "保存为 Markdown"
10  },
11  "content_scripts": [
12    {
13      "matches": ["<all_urls>"],
14      "js": ["vendor/turndown.js", "content.js"],
15      "run_at": "document_idle"
16    }
17  ]
18}

几个关键点:

1. chrome.tabs.sendMessage vs chrome.runtime.sendMessage

  • chrome.runtime.sendMessage:发给 background service worker
  • chrome.tabs.sendMessage:发给某个 tab 里的 content script
  • popup 给 content script 发消息必须用 chrome.tabs.sendMessage

2. 异步响应要 return true

content script 的 onMessage 监听器如果要异步响应(用 await、setTimeout 等),必须 return true,否则 sendResponse 通道会被关闭。

3. 消息体是 JSON

sendMessage 的参数和返回值都必须是 JSON 兼容的数据。函数、DOM 节点、Symbol 传不过去。

4. 第一次用 downloads API 会弹权限申请

用户第一次点"保存为 Markdown"时,Chrome 会弹窗问"是否允许 Markdown Saver 下载文件"。同意后就能用。

验证:

  1. 重新加载插件
  2. 刷新当前网页
  3. 点工具栏图标
  4. 点"保存为 Markdown"
  5. 应该弹出文件保存对话框,默认文件名是 页面标题_日期.md

如果出问题:

  • popup 看不到 console:右键 popup → 检查
  • content script 报错:刷新页面,看 F12 控制台
  • 消息没发出去:检查 manifest 里 content_scripts 的 matches 是否覆盖当前 URL

触发下载和保存用户偏好

现在加上用户偏好。用 chrome.storage.local 存,比 localStorage 靠谱。

chrome.storage vs localStorage:

  • localStorage:每个 origin 独立。content script 注入的页面有自己的一套 localStorage,跨页面、跨插件读不到。
  • chrome.storage:异步 API,跨页面、跨插件、跨设备(如果用 sync)共享。
  • chrome.storage.local 就行,sync 需要登录 Chrome 账户,不必要。

popup.js(加入偏好读取和应用):

 1const saveBtn = document.getElementById("save");
 2const statusEl = document.getElementById("status");
 3const includeImages = document.getElementById("includeImages");
 4const includeCode = document.getElementById("includeCode");
 5
 6// 加载保存的偏好
 7chrome.storage.local.get(["includeImages", "includeCode"], (result) => {
 8  includeImages.checked = result.includeImages !== false; // 默认 true
 9  includeCode.checked = result.includeCode !== false;     // 默认 true
10});
11
12// 监听变化并保存
13includeImages.addEventListener("change", () => {
14  chrome.storage.local.set({ includeImages: includeImages.checked });
15});
16includeCode.addEventListener("change", () => {
17  chrome.storage.local.set({ includeCode: includeCode.checked });
18});
19
20saveBtn.addEventListener("click", async () => {
21  saveBtn.disabled = true;
22  statusEl.textContent = "提取中...";
23
24  try {
25    const [tab] = await chrome.tabs.query({ active: true, currentWindow: true });
26    const response = await chrome.tabs.sendMessage(tab.id, { action: "extract" });
27
28    if (!response || !response.markdown) {
29      statusEl.textContent = "提取失败";
30      return;
31    }
32
33    let markdown = response.markdown;
34
35    // 根据用户偏好过滤
36    if (!includeImages.checked) {
37      markdown = markdown.replace(/!\[.*?\]\(.*?\)/g, "");
38    }
39    if (!includeCode.checked) {
40      markdown = markdown.replace(/```[\s\S]*?```/g, "");
41    }
42
43    const title = document.title.replace(/[\\/:*?"<>|]/g, "_");
44    const date = new Date().toISOString().slice(0, 10);
45    const filename = `${title}_${date}.md`;
46
47    await chrome.downloads.download({
48      url: "data:text/markdown;charset=utf-8," + encodeURIComponent(markdown),
49      filename: filename,
50      saveAs: true
51    });
52
53    statusEl.textContent = "已保存";
54  } catch (err) {
55    statusEl.textContent = "错误:" + err.message;
56  } finally {
57    saveBtn.disabled = false;
58  }
59});

到这一步,插件的核心功能完整了。能提取、能转换、能下载、能存用户偏好。

Manifest V3 必须知道的几件事

最终的 manifest.json:

 1{
 2  "manifest_version": 3,
 3  "name": "Markdown Saver",
 4  "version": "0.1.0",
 5  "description": "一键把当前页面存为 Markdown",
 6  "permissions": ["downloads", "storage"],
 7  "host_permissions": ["<all_urls>"],
 8  "action": {
 9    "default_popup": "popup.html",
10    "default_title": "保存为 Markdown",
11    "default_icon": {
12      "16": "icons/icon16.png",
13      "32": "icons/icon32.png",
14      "48": "icons/icon48.png",
15      "128": "icons/icon128.png"
16    }
17  },
18  "icons": {
19    "16": "icons/icon16.png",
20    "32": "icons/icon32.png",
21    "48": "icons/icon48.png",
22    "128": "icons/icon128.png"
23  },
24  "content_scripts": [
25    {
26      "matches": ["<all_urls>"],
27      "js": ["vendor/turndown.js", "content.js"],
28      "run_at": "document_idle"
29    }
30  ]
31}

几个关键字段:

action(V3 替换了 browser_action / page_action)

V2 时代有 browser_action(浏览器全局按钮)和 page_action(特定页面按钮)。V3 统一成 action

service_worker(我们没用)

V3 用 service_worker 取代常驻 background page。我们这个插件用不到,因为 popup 直接跟 content script 通信,不需要经过后台。

如果你的插件需要跨页面协调(比如同时管理多个 tab),才需要 service_worker。

host_permissions(V3 独立字段)

V2 时代权限混在 permissions 里。V3 把"能访问哪些网站"拆出来。

"<all_urls>" 表示所有网站。如果你只想在某些网站用,改成 ["https://*.example.com/*"]

icons(必填,4 个尺寸)

Chrome Web Store 严格要求 4 个尺寸:16、32、48、128。缺一个都不让过。

可以自己画,可以找设计师,可以去 iconmonstr 这种免费图标站下载。PNG 格式,背景透明。

上架到 Chrome Web Store

如果只想给自己用,刷新插件就够了。要发出去让别人也能装,需要走 Chrome Web Store。

1. 注册开发者账号

打开 Chrome Web Store Developer Dashboard,点"创建开发者账户"。

需要:

  • 5 美元一次性注册费
  • Visa 卡或其他支持的信用卡
  • 科学上网(国内访问 Google 服务需要)

注册一次,永久有效。

2. 打包插件

把整个 markdown-saver 文件夹压缩成 zip。注意:

  • 直接压缩文件夹内容,不是压缩文件夹本身
  • 不要包含 .DS_StoreThumbs.db 这种系统文件
  • 不要包含源代码注释里写的 TODO 之类的敏感信息
1# 在项目根目录
2zip -r markdown-saver.zip . -x "*.DS_Store" "*/Thumbs.db"

3. 准备商店材料

Chrome Web Store Developer Dashboard,点"新增项目"。

需要填:

  • 插件包:上传刚才的 zip
  • 商店图标:128x128 PNG,必须
  • 小宣传图(可选):440x280
  • 大宣传图(可选):920x680
  • 截图:1280x800 或 640x400,至少 1 张,最多 5 张
  • 简短描述:最多 132 字符,简洁说明插件做什么
  • 详细描述:完整功能介绍、支持的使用场景
  • 类别:工具 / 生产力 等
  • 语言:默认英文,可加多语言
  • 隐私声明 URL:必填
  • 权限说明:每个权限都要解释为什么用

隐私声明

需要一个公开的 URL。可以放在:

  • GitHub Pages
  • 一个简单的 Notion 公开页
  • 自己博客的一个静态页面

内容要说明:

  • 插件收集什么数据(不收集最好,写"不收集任何数据")
  • 收集的数据做什么用
  • 第三方服务(这个插件没用到)

4. 提交审核

填完所有材料,点"提交审核"。

审核时间:

  • 首次提交:1-3 天
  • 被打回修改后重提:通常 1 天内
  • 紧急更新:几小时内

5. 常见打回原因

1)缺少隐私声明 URL

必填。没有就打回。

2)权限超出实际需要

如果你申请了"读取所有网站数据"但实际不需要,审核员会打回。"host_permissions": ["<all_urls>"] 申请了就一定要在描述里解释清楚。

3)single purpose 不明确

一个插件做太多事会被打回。Markdown Saver 目的明确:把页面转成 Markdown。一个插件同时改页面 + 抓 cookie + 拦截广告,肯定不过。

4)icon 用了禁用素材

Google 商标、Chrome 标志、其他产品的 Logo 不能用。

5)截图或描述夸大

“最好用的 Markdown 工具” 这种话容易被打回。要客观描述功能。

6. 被打回怎么改

审核员会在邮件里写明原因。常见流程:

  1. 看邮件里的原因
  2. 修改对应问题
  3. 重新上传 zip(在 dashboard 里编辑现有项目)
  4. 重新提交

如果是权限问题被卡,可以考虑:

  • 缩小 host_permissions 范围(从 <all_urls> 改成具体网站)
  • 在隐私声明里详细说明

7. 怎么更新版本

发布后想改代码:

  1. 改完代码
  2. 修改 manifest.json 里的 version(必须递增,比如 0.1.0 → 0.2.0)
  3. 重新打包 zip
  4. 在 dashboard 里编辑现有项目,上传新 zip
  5. 重新提交审核

更新版本审核通常 1 天内。

调试常见问题

1. service worker 的 console.log 在哪看

如果你的插件有 service worker,它在普通 DevTools 里看不到日志。要去 chrome://extensions,点插件的"service worker"链接,会打开一个独立的 DevTools 窗口。

我们的插件没用 service worker,跳过。

2. popup 的 console 在哪看

右键点 popup 的任意位置,选"检查"。会打开一个 DevTools 窗口,专门给这个 popup 用。

3. content script 报错怎么 debug

content script 报错会显示在目标网页的 DevTools 里。但它有独立的"扩展视图":

  • F12 打开 DevTools
  • 顶部下拉框,切换到你的插件名(不是当前的网页)

4. 修改了代码没生效

Chrome 会缓存插件代码。改完代码后:

  • 必须回 chrome://extensions 点刷新按钮
  • 改 manifest 里的字段(如 content_scripts)尤其要刷新

5. 消息没发出去

在 popup 控制台或 content script 控制台分别 console.log,看是哪一边没收到。常见原因:

  • chrome.tabs.sendMessage 的 tabId 写错
  • content script 没注入(检查 matches 是否覆盖当前 URL)
  • manifest 里 content_scripts 改完忘了刷新插件

还能做什么

这个版本是基础骨架。能扩展的方向:

1. 快捷键触发

chrome.commands API 让你绑定一个键盘快捷键,触发保存。在 manifest.json 加:

1"commands": {
2  "save-as-markdown": {
3    "suggested_key": {
4      "default": "Ctrl+Shift+S"
5    },
6    "description": "保存当前页面为 Markdown"
7  }
8}

然后在 background service worker 里监听 chrome.commands.onCommand

2. 右键菜单触发

chrome.contextMenus API。右键点击页面空白处,菜单里加一项"保存为 Markdown"。

3. 图片下载到本地

当前版本只把图片 URL 转成 Markdown。如果要下载图片到本地或 base64 内嵌,需要:

  • fetch 下载图片
  • 转成 base64
  • 替换 Markdown 里的图片 URL

4. 自定义选择器

让用户自己指定要保留或排除的元素。加一个 options 页面(chrome.runtime.openOptionsPage())。

5. 适配 Firefox

Firefox 也支持 WebExtensions API,但有些差异:

  • host_permissions 不需要分出来
  • background.scripts 替代 service_worker(Firefox 还没完全迁移)
  • API 行为略有不同

不是简单改个 manifest 就能跑,需要测试和适配。

6. 适配 Edge

Edge 用的就是 Chrome 内核,能直接装 Chrome Web Store 的插件。不用单独适配。

写在最后

这个插件不长,但走完了 Chrome 插件开发的完整流程:

  • manifest.json 是什么、怎么配
  • content script 怎么注入、怎么拿页面内容
  • popup 怎么和 content script 通信
  • chrome.storage 怎么存用户偏好
  • chrome.downloads 怎么触发下载
  • Manifest V3 跟 V2 差在哪
  • Chrome Web Store 怎么注册、怎么提交、怎么过审核

实际写一个能解决自己问题的插件,整个流程大概一两天。前 80% 的时间花在踩坑上,后 20% 才是写业务代码。这篇文章能帮你跳过那 80% 的坑。

剩下的就是动手了。打开编辑器,从第一个 manifest.json 开始。

以本文发布时的 Chrome Web Store 政策为准。截图规格、描述字数、隐私政策要求可能会调整。提交前看一眼 Chrome Web Store 官方文档 确认。

发表评论