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");
加载插件:
- 打开
chrome://extensions - 点左上角"加载已解压的扩展程序"
- 选你的
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 和动态注入的脚本之间的通信
最干净的方式是:
- content script 注入 Turndown 脚本
- content script 通过
window.postMessage与页面脚本通信 - 或者更直接:让 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,先在控制台手动测试:
- 重新加载插件(
chrome://extensions→ 刷新按钮) - 刷新当前网页
- 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 workerchrome.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 下载文件"。同意后就能用。
验证:
- 重新加载插件
- 刷新当前网页
- 点工具栏图标
- 点"保存为 Markdown"
- 应该弹出文件保存对话框,默认文件名是
页面标题_日期.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_Store、Thumbs.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. 被打回怎么改
审核员会在邮件里写明原因。常见流程:
- 看邮件里的原因
- 修改对应问题
- 重新上传 zip(在 dashboard 里编辑现有项目)
- 重新提交
如果是权限问题被卡,可以考虑:
- 缩小
host_permissions范围(从<all_urls>改成具体网站) - 在隐私声明里详细说明
7. 怎么更新版本
发布后想改代码:
- 改完代码
- 修改
manifest.json里的version(必须递增,比如 0.1.0 → 0.2.0) - 重新打包 zip
- 在 dashboard 里编辑现有项目,上传新 zip
- 重新提交审核
更新版本审核通常 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 官方文档 确认。
发表评论