2026-01-26 08:27:04
在之前的 7 篇博客中,我们依次了解了一些扩展开发中的基本概念,并且每一篇都附上了一个扩展示例。现在,我们终于要演示如何发布扩展了。下面我们将演示如何将之前做的自动冻结不活跃标签页的那个扩展发布到 Chrome Web Store 中,还记得吗?就是我们在 Browser Extension Dev - 04. Background Script 和 Browser Extension Dev - 05. 存储和配置 中作为示例的那个扩展。
注册完成后打开 https://chrome.google.com/webstore/devconsole/ 应该可以看到如下页面。

接下来,开始演示如何从构建到最终发布扩展。
首先,在项目中打开终端,然后运行 pnpm zip,应该会看到类似下面这样的输出,可以看到 Chrome 扩展已经被正常打包成 zip。
1 |
|
在 .output 目录下找到这个文件,记住这个路径。

然后打开 https://chrome.google.com/webstore/devconsole/ 并点击右上角的 New Item 按钮。

选择刚刚找到的 zip 文件上传,此时遇到了一个错误,提示 The manifest has an invalid version: 0.0.0. Please format the version as defined,也就是版本号不能为 0.0.0

使用 pnpm version patch 将版本号增加到 0.0.1,然后重新运行 pnpm zip 构建并上传,即可看到扩展发布管理页面。

其中,对于发布而言,最重要的两个标签页是 Store listing 和 Privacy。前者用于配置扩展在 Chrome Web Store 中的展示信息,例如简介、分类、图标和截图等等,后者则是权限使用说明和隐私政策链接。
对于这个扩展而言,选择分类为 Productivity > Tools,语言选择 English。

然后从 public/icon 目录选择 128.png 图标作为在商店显示的扩展图标。要截取精确 1280x800 像素的截图可能有点麻烦,但可以直接使用 https://squoosh.app 来调整截图的大小,使用 Resize 功能调整截图尺寸到 1280x800 就好了。

PS: 如果你使用 mac,可以使用小工具 Window Resizer 来将窗口尺寸修改为指定大小。
参考 Chrome 官方发布文档 https://developer.chrome.com/docs/webstore/publish
切换到 Privacy 标签,可以看到有几个主要区域
Are you using remote code?,Chrome 禁止使用远程代码,某些库(如 zod)可能会不小心引入远程代码,需特别留意

按下 Save draft 按钮之后,如果 Submit for review 按钮可用,那就说明可以提交扩展进行审核了。

提交审核后,将会进入审核队列,通常需要几天甚至更长时间进行初次审核,所以还请耐心等待,某些使用高风险权限(例如向所有网站注入脚本)或者针对高风险网站(当然,吾辈是在说 YouTube)的扩展可能需要等待更长时间。

至此,浏览器扩展开发的基础内容就介绍完了。后续可能会有一些进阶主题的番外篇,比如国际化、GitHub Actions 自动发布等。
如果还对发布 Safari 扩展并上架 App Store 感兴趣,可以查看我之前写的博客 转换 Chrome Extension 为 Safari 版本 和 发布 Safari 扩展到 iOS 应用商店。提醒一下,这非常复杂,且开发者账户无试用期,必须满足 1)有一台 macOS 电脑并且安装 Xcode 等开发工具 2)开通 App Store 开发者账户并支付 $99/年的费用。
2026-01-21 22:33:45
在上一章 Browser Extension Dev - 06. Inject Script on Demand 中,我们介绍了按需为网页注入脚本执行自定义的功能,还实现了一个简单的复制网页主要内容为 Markdown 的扩展。在这一章中,我们将继续实现一个 Popup 弹窗,用于显示页面主要内容转换得到的 Markdown,并支持在复制之前进行预览和编辑。
首先,需要明确 Popup 是什么?
之前我们已经接触过 Content Script 注入网页的 UI 和 Options 配置页面。Popup 类似于 Options 页面,独立运行,但权限相比 Background/Options 更加受限。通常而言,它和 Content Script UI 的应用场景非常接近,都是显示一些当前网页相关的内容,但它也有一些独有的适用场景:
Content Script UI 则有其他几个优势
接下来,让我们接着之前的实现继续完善吧。
参考 Chrome 官方文档 https://developer.chrome.com/docs/extensions/develop/ui/add-popup
现在面临一个问题:如何在 Popup 中获取页面的内容?
答案是无法直接获取,需要通过 Background Script 中转,大致流程如下:
Popup → Background → executeScript(inject.js) → 返回 markdown → Popup 显示
但是等等,scripting.executeScript 可以有返回值吗?当然可以,它支持同步和异步返回值,但返回值必须是可结构化克隆的。
参考 Chrome scripting API 关于 Promise 返回值的官方文档 https://developer.chrome.com/docs/extensions/reference/api/scripting#promises
首先添加一个 popup 页面,在 entrypoints/popup 下添加 index.html 和 main.ts
1 |
|
1 |
|
在浏览器中加载扩展之后,点击 action 可以看到弹窗出现了。

在实现通信部分之前,需要修改一下之前注入的 Inject Script,不再复制 Markdown 到剪切板,而是使用 return 返回给调用者。
1 |
|
下面开始实现 Popup 与 Background Script 的通信部分,由于 Chrome 原生的通信 API 使用起来非常痛苦,这里使用一个浅包装 @webext-core/messaging。
安装依赖
1 |
|
然后在 lib/messager.ts 中定义接口
1 |
|
然后在 Background Script 定义实现
1 |
|
最后在 Popup 中调用,出于简化考虑,这里直接使用 pre 渲染了 Markdown,我们将在下一步引入所见即所得的 Markdown 编辑器。
1 |
|

由于并未使用 react,所以这里直接使用一个 vanilla JS 实现的 markdown 编辑器 easymde。
还是先安装依赖。
1 |
|
然后在 Popup 中使用它。
1 |
|
现在就可以看到最终的效果了。

在这一章,我们介绍了 Popup 的应用场景、Popup 与 Background Script 的通信、以及从网页获取数据的功能与实现。在下一章,我们终于要发布插件了,我将演示如何将插件发布到 Chrome Web Store,以便让其他人也能使用开发的扩展。
如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2
完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/07-popup-ui
2026-01-21 10:57:21
在上一章 Browser Extension Dev - 05. Storage and Configuration 中,我们介绍了为扩展添加设置页面并使用 Storage API 保存和读取配置的功能。在这一章,我们将介绍按需注入脚本。这种方式完全不会拖慢网站运行速度,同时在 Chrome Web Store 安装时不会有任何安全警告提示。接下来我们将实现一个扩展:点击图标即可将网页主要内容复制为 Markdown。
我们之前已经接触过 Content Script 注入和 Background Script 监听扩展图标点击。虽然尚未介绍,但两者可以进行消息通信。
有了上面的背景知识,你可能会想要这样做:
这种方法有几个主要缺点:

而对于需要明确动作触发的场景,其实有更简单的实现方式:
这样,需要的权限就从 ['<all_urls>'] 变成了 ['activeTab', 'scripting'],虽然权限数量变多了,但风险反而更低,必须由用户触发才能执行代码,所以安装扩展时不会有任何警告。例如:

这里涉及到的关键 API 是 scripting.executeScript,顾名思义,用于执行自定义脚本。
参考 Chrome 官方 activeTab 指南:https://developer.chrome.com/docs/extensions/develop/concepts/activeTab
接下来在后台脚本中实现监听和注入部分。
首先还是更新 wxt.config.ts,添加所需权限以及空的 action 字段。
1 |
|
然后添加一个用于测试的注入脚本。与 Content Script 不同,这类脚本在 WXT 中需要使用 defineUnlistedScript 声明。
1 |
|
然后在 background 中监听并注入它。
1 |
|
除了 files 参数,还可以通过 func/args 直接传递函数和参数,适用于简单场景。参考:https://developer.chrome.com/docs/extensions/reference/api/scripting#type-ScriptInjection
当我们打开 google.com 然后点击扩展图标时,却发现没有反应。查看扩展详情页面,可以看到一个错误。
1 |
|
和之前 Browser Extension Dev - 03. 注入 UI 时一样,需要在 manifest 中增加 web_accessible_resources 配置。
1 |
|
然后再试一次,可以看到脚本确实被注入并正确执行了。

⚠️ 局限性:如果你希望注入的脚本能持久化(刷新或重新进入页面后仍自动运行),这是行不通的,仍然需要正确声明
host_permissions权限来持久化注入 Content Script,即使使用 scripting API 仍然受到权限模型的限制。
接下来实现读取网页主要内容,转换为 Markdown,然后复制到剪贴板的功能。借助 npm 生态,实现起来非常简单。
首先安装需要的依赖
1 |
|
然后编写少量胶水代码即可完成。
1 |
|


在本章中,我们实现了一个按需注入脚本的扩展,它不会影响网页正常运行,只在用户触发时才执行代码,真正做到即插即用。在下一篇,我们将继续完善这个扩展,使用 Popup 弹窗直接预览和编辑从当前页面复制的 Markdown。
如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2
2026-01-16 17:14:40
在上一章 Browser Extension Dev - 04. Background Script 中,我介绍了 Background Script 的概念和使用场景,并实现了一个自动休眠不活跃标签页的扩展。在本章,我将介绍如何在扩展中存储数据和配置选项,并提供一个配置页面来访问它。
浏览器为扩展提供了 browser.storage API,允许存储 kv 数据,可以存储任何可以被结构化克隆的数据,通常而言对于扩展的配置功能足够了。除此之外,有时候还使用 localStorage(如果是 Content Script)或 indexeddb(简单的 kv 存储不足以满足需求时)来存储扩展的设置。
其中 browser.storage API 下有几个选项,它们的接口是一致的,只是存储的方式和行为有些不同
参考 Chrome 官方文档: https://developer.chrome.com/docs/extensions/reference/api/storage
配置页面是浏览器为扩展提供的一个专用页面,允许在单独的页面中调整扩展的选项,或者访问扩展提供的功能。下面是两种配置页面的使用方式
直接使用浏览器内嵌页面访问,布局紧凑,适合配置项较少的情况,也是官方推荐的默认方式。
或者在独立标签页中打开,有更大的空间展示完整配置甚至功能,但需要额外配置或编写代码才能让用户方便地访问。
在 WXT 中,可以在 options.html 中添加 meta 标签来修改它,参考 https://wxt.dev/guide/essentials/entrypoints.html#options
1 |
|
同时,也有两种方法可以访问扩展的配置页面

参考 Chrome 官方文档: https://developer.chrome.com/docs/extensions/develop/ui/options-page
在 WXT 中,需要在 entrypoints 目录下添加 options.html 或者 options/index.html 文件。
1 |
|
WXT options entrypoint 文档: https://wxt.dev/guide/essentials/entrypoints.html#options
效果:

创建 entrypoints/options/main.ts 并在 html 的 body 标签末尾引入。
1 |
|
1 |
|
打开配置页面测试,发现功能没有生效。右键打开开发者工具,在控制台中看到以下错误:
1 |
|
这是因为缺少 storage 权限。使用需要权限的 API 之前都必须先声明,修改 wxt.config.ts 添加权限:
1 |
|
现在,修改页面中的 Auto Sleep Interval 选项的值之后,刷新页面,可以看到值已经被持久化了。

不过,HTML 默认样式实在太丑了,让我们引入 tailwindcss 并添加一些样式。
安装依赖
1 |
|
更新配置并添加 tailwindcss 插件。
1 |
|
然后在 html 中引入 tailwindcss 美化一下。
1 |
|
现在,我们可以看到效果至少好看了一点。

将硬编码的 Timeout 改为从 storage 读取:
1 |
|
如果需要在修改配置后立刻触发重新检测,还可以使用 storage.onChanged API,由于上面已经监听了标签页切换时自动触发检测,所以下面这段代码只做演示。
1 |
|
目前为止,我们都使用 Chrome 默认的方法打开配置页面,例如上面提到的两种方法。但其实我们还可以将点击浏览器右上角的 action 图标绑定到打开配置页面的行为。
首先在 wxt.config.ts 的 manifest 中声明 action 选项,目前留空即可。
1 |
|
然后在 background script 中监听 browser.action.onClicked 事件
1 |
|
现在,只要点击 action 就能打开配置页面,更加方便快捷。
在这一篇中,主要介绍了添加配置页面以及使用 storage API。在下一篇中,将介绍按需向网页注入脚本,也将是目前为止唯一一个在 Chrome Web Store 安装扩展时不会有任何警告信息的扩展。
如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2
2026-01-14 23:32:29
在上一章 Browser Extension Dev - 03. 注入 UI 中,我介绍了如何向网页中注入自定义的 UI,同时还了解了如何使用 Shadow DOM、Tailwind CSS 和使用 npm 包。在本章,我将介绍 Background Script,这是扩展的核心元素之一。
首先,什么是 Background Script?
顾名思义,这是扩展可以在后台运行的脚本,与注入到网页的脚本有所不同,它有几个显著的特点:
参考:
Background 简介:https://developer.chrome.com/docs/extensions/develop/concepts/service-workers(Manifest V3 后 Chrome 官方改名为 extension service workers,但通常还是习惯称呼为 Background Script)
Manifest V3 简介:https://developer.chrome.com/docs/extensions/develop/migrate/what-is-mv3
上面就是 Background Script 的几个关键点,接下来我将实现一个自动休眠不活跃标签页的扩展来演示它,下面是将会涉及到的 Tabs API,参考 https://developer.chrome.com/docs/extensions/reference/api/tabs
首先,我们该如何定义不活跃的标签页?
从简单的角度来说,一个长时间没有访问的标签页就是不活跃的,例如最后一次访问标签页还是 30 分钟之前,那么应该可以认为是不活跃的了。如何知道标签页的最后访问时间呢?这就需要监听标签页相关的事件了,下面是涉及到的三个基本事件:
onCreated => 将标签页信息添加到扩展记录中
onRemoved => 从扩展记录的标签页列表中移除
onActivated => 更新标签页的最后访问时间
在长时间不活跃之后,我们可以自动冻结它,浏览器允许在不关闭标签页的情况下自动从内存中驱逐,再次访问时会自动重新加载。
1 |
|
注意:Tabs API 本身提供了
lastAccessed字段查看标签页的最后访问时间,但这个字段在 Safari 浏览器并不支持,参考 https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API/tabs/Tab#browser_compatibility
由于在之前的章节中已经说明过如何初始化扩展,这里不再赘述初始化过程。
1 |
|
1 |
|
首先打开 entrypoints/background.ts,可以看到 WXT 初始化的代码。
1 |
|
让我们在函数中注册我们的监听器
1 |
|
现在,使用 pnpm dev 启动开发模式,打开 chrome://extensions/,加载已解压的扩展并选择 .output/chrome-mv3-dev 目录,然后点击扩展的 service worker 链接打开 Background Script 的 DevTools Console 开始调试。


当我们添加一个新标签页时可以看到 Tab activated: 1207047510 相关的日志。

然后我们需要在 onActivated 事件中检查已记录的标签页中是否有长时间未访问的标签页,如果发现就自动冻结它。在此之前,需要更新 wxt.config.ts 添加 tabs 权限,这是使用 browser.tabs.query API 所必须的。
1 |
|
你可能注意到,下面查找标签页时包含了很多过滤条件,下面将会一一说明
1 |
|
将 Timeout 调整为 1ms,就可以方便的进行测试了。在切换标签页再切换回来之后,就能看到标签页自动刷新了,这意味着自动冻结功能确实生效了。

现在,我们实现了基本的标签页自动冻结功能,这个实现非常粗糙,还有很多问题没有处理,如果感兴趣,你可以自行尝试解决下面几个问题:
browser.runtime.onStartup)在下一章,我们将介绍配置相关的 API 和 options 页面来解决配置问题。
如果有任何问题,欢迎加入 Discord 群组讨论。
https://discord.gg/VxbAqE7gj2
完整代码:https://github.com/rxliuli/browser-extension-dev-examples/tree/main/packages/04-background-script
2026-01-06 20:44:07
最近因为多邻国 App 的 Bug,我正在考虑切换到 Web 版本。但没想到移动端 Web 版本体验那么差,每次打开首页都会弹出 App 下载推广,这真的太烦人了。我第一时间就想去写个 UserScript 彻底删除它,但没想到还碰到一些有趣的问题。

从 DevTools > Elements 中很容易就找到了弹窗元素的稳定选择器。
1 |
|
页面遮罩也很容易定位。
1 |
|
然后我就可以正常点击了,但我仍然无法自由滚动,而滚动条锁定又不在 HTML/body 上,所以这种情况下应该如何找到是哪个 HTML 元素导致的滚动条锁定呢?

想要解决这个问题需要一些简单的启发式的方法。首先,一般如何实现滚动条锁定?
通常只需要给 body 元素增加一些样式即可,例如
1 |
|
或者使用 position: fixed
1 |
|
按照这种思路,我们可以找到一个在无法滚动区域内的元素,然后递归向上查找,直到找到 overflow: hidden; 或者 position: fixed 的元素即可认为它就是阻止滚动的罪魁祸首,实现起来相当简单。
1 |
|
视频演示
现在,我们可以通过 CSS 恢复正常的滚动条了。
tip:
$0在 DevTools > Console 中代表在 DevTools > Elements 面板中选中的元素。
如果你也想尝试开发 Extension 或者 UserScript 来改善你的 Web 体验,不妨一试,JavaScript 提供了相当多 trick 的技巧,如果只是编写普通的 Web App,可能永远不会有机会尝试。