背景 今天在修复一个 Redirector 扩展 [1] 的一个错误之后,推送了一个版本触发 CI 构建并使用 extport [2] 全平台发布,在发布 extport 的时候出现了一个神秘的错误。 1 macos: submission item add failed (409): { "errors" : [ { "id" : "607f4cc4-96af-4d99-ae60-78bb87489e8a", "status" : "409", "code" : "STATE_ERROR.ENTITY_STATE_INVALID", "title" : "appStoreVersions with id '652e9da7-e89…
阅读全文
02前言 昨天在尝试为一个扩展程序增加 Safari 支持的时候,发现了一个神奇的问题,在 popup 中滚动感觉很迟缓。 就像是这样,滚动速度不仅缓慢,而且没有任何惯性效应。 imp-rec-2026-08-07-105253.mp4 如果在新标签页打开 popup 页面,你可以看到滚动要流畅的多 imp-rec-2026-08-07-110547.mp4 猜测 具体来说,复现步骤很简单 使用 JavaScript 写入大量内容产生滚动条,上面的示例中使用了一个相当简单的 for 循环 + ul list 打开 popup 弹窗,滚动就可以触发这个问题 1 2 3 4 5 6 7 8 // popup.js const ul = do…
阅读全文
03背景 在 使用 GitHub Actions 全自动发布 Safari 扩展 里,我写过一套在 macOS runner 上用 Xcode 云签名(-allowProvisioningUpdates)自动构建、签名、上传 Safari 扩展的流程。流程本身是能跑起来的,但在跑通之前,我踩过两个坑,而且巧的是,这两个坑都有同一个特点:报错信息含糊不清,搜索引擎上几乎搜不到答案。这篇文章把它们记录下来,包括现象、原因和修复方法。 两个坑追根溯源是同一件事:无状态的 CI runner,打破了苹果签名工具原本假设的前提。 陷阱一:「maximum number of certificates」 场景是这样的:GitHub 提供的 maco…
阅读全文
04背景 我之前写过两篇关于 Safari 扩展的博客:转换 Chrome Extension 为 Safari 版本,以及 发布 Safari 扩展到 iOS 应用商店。它们解决的是”第一次怎么做成”。但随着我的 Safari 扩展越来越多,每次发版的手动流程变得越来越难以忍受:打开 Xcode、改版本号、Archive、Validate、Distribute,再打开 App Store Connect 选 bundle、填 release note、点提交——每个扩展十分钟起步,而且不能出错。 这篇文章介绍我现在的发布流程:改一下 package.json 里的 version,push 到 GitHub,然后就没有然后了。构建、签…
阅读全文
05前言 自去年年底去过一次之后,我就喜欢上了在海岛上旅行的感觉,相比于大陆近岸的海水,海岛周围的颜色透着一种天空的蓝。 环岛徒步 这次旅行又走了一圈环岛徒步,只花了不到 3 个小时,比上次要快得多。 碧蓝之海,启动! 远方还能看到几艘渔船。 靠近岸边的海水反而泛着一些浅绿色。 顺时针走到大约 1/4 的地方,可以看到一个废弃的村庄,似乎目前只然有一些海防人员在此居住。 回头望去,几座岛屿在海平线的地方,却是无法上去的无人岛。 无论何时何地,总会不经意间看到大橘的身影。 一直都很喜欢这种海水不断拍打着礁石的画面。 正午,整个海平面都泛着白光。 远方矗立着一个不知名的三角形箭头,不知道是做什么的。 似乎还有人居住的村庄,但也主要集中在东北…
阅读全文回来一个星期了,才开始动笔写新加坡和越南的旅行。相比于呆了 24 天的马来西亚,新加坡和越南都只呆了 3 天,尤其是因为越南航空晚点 16 个小时,导致越南之行实际上只有 2 天时间,非常匆忙。 新加坡 从新山走陆路口岸通过,如预料之中,新加坡的电子化极其发达,包括海关都是直接刷护照即可自动通关,完全没有人工环节。公交或地铁也都可以直接刷 Visa 卡,尽管国内的 Visa 双币卡在国外就是废物一条着实坑了我,但仍然难以掩盖信息化的发达。 新加坡 - 市区 住在市中心附近,所以下午先去逛了会街,但大部分小店的商品总感觉是从义乌直接批发的。 走了一会就能看到地标建筑金沙酒店,三栋楼上面放着一艘船的样子。 沿着河边走,还能看到远处已经停…
阅读全文自从之前的亚庇之行结束后,我就出发前往了仙本那。那是一个很小的小镇,基础设施极其糟糕,而且总体上似乎相当贫困,路边经常看到乞讨者,小孩子围着人转希望给她们钱的例子屡见不鲜。海洋看起来相当不错,但烈日极其毒辣,我刚到的第三天就已经被晒脱皮了,不过似乎主要活动是潜水,如果不喜欢它,可能也不太值得待太长时间。之后前往了新山,临近新加坡的第二大都市圈,但对我而言过于熟悉了,所以呆的两天基本没出去,尤其是第二天只是呆在住的地方,几乎不值一提。 仙本那 - 市区 到住处暂存行李之后就开始在镇上闲逛,偶然看到河边的“水上人家”,大量简陋的铁皮屋矗立在水面上。完全缺乏管道系统(下水道)是最大的问题,而且小孩子还在旁边的水里游泳,但水质极其糟糕。 岸…
阅读全文前言 在春节之后,我来到了马来西亚旅行。这是我出国旅行的第三个国家。在此之前,我曾经在日本呆了半年,在关西走了一些地方,回国后又花了几个月的时间走完了整个中国的所有省份和直辖市,所以某种程度上我的阈值确实提高了。因此,直到目前为止,马来西亚给我的体验,可以用一个字来形容:烂。 吉隆坡 落地首先到了吉隆坡,基建水平还行,但室内导航很困难,而且售票机只收取小额纸币导致我一度没有零钱可用,直到后面办了一张 TNG 的卡,但这张卡在槟城时公交完全用不了,后面的其他地方几乎没有公共交通可用,所以是的,基本上是个坑。去看了那个大佛像和彩色楼梯,确实不错,至少值得一去。 吉隆坡标志性的大佛像。 彩色楼梯栏杆上的猴子。 登至台阶顶处往上看。 独立纪…
阅读全文前言 在之前的 7 篇博客中,我们依次了解了一些扩展开发中的基本概念,并且每一篇都附上了一个扩展示例。现在,我们终于要演示如何发布扩展了。下面我们将演示如何将之前做的自动冻结不活跃标签页的那个扩展发布到 Chrome Web Store 中,还记得吗?就是我们在 Browser Extension Dev - 04. Background Script 和 Browser Extension Dev - 05. 存储和配置 中作为示例的那个扩展。 步骤 准备 Chrome Web Store 发布账户 首先按照官方文档注册开发者账户,需要一个 Google 账户并且支付一次性注册费用 $5 即可完成。参考:https://devel…
阅读全文
10前言 在上一章 Browser Extension Dev - 06. Inject Script on Demand 中,我们介绍了按需为网页注入脚本执行自定义的功能,还实现了一个简单的复制网页主要内容为 Markdown 的扩展。在这一章中,我们将继续实现一个 Popup 弹窗,用于显示页面主要内容转换得到的 Markdown,并支持在复制之前进行预览和编辑。 首先,需要明确 Popup 是什么? 之前我们已经接触过 Content Script 注入网页的 UI 和 Options 配置页面。Popup 类似于 Options 页面,独立运行,但权限相比 Background/Options 更加受限。通常而言,它和 Cont…
阅读全文
11前言 在上一章 Browser Extension Dev - 05. Storage and Configuration 中,我们介绍了为扩展添加设置页面并使用 Storage API 保存和读取配置的功能。在这一章,我们将介绍按需注入脚本。这种方式完全不会拖慢网站运行速度,同时在 Chrome Web Store 安装时不会有任何安全警告提示。接下来我们将实现一个扩展:点击图标即可将网页主要内容复制为 Markdown。 思考 我们之前已经接触过 Content Script 注入和 Background Script 监听扩展图标点击。虽然尚未介绍,但两者可以进行消息通信。 有了上面的背景知识,你可能会想要这样做: 为所有网页…
阅读全文
12前言 在上一章 Browser Extension Dev - 04. Background Script 中,我介绍了 Background Script 的概念和使用场景,并实现了一个自动休眠不活跃标签页的扩展。在本章,我将介绍如何在扩展中存储数据和配置选项,并提供一个配置页面来访问它。 Storage API(概念) 浏览器为扩展提供了 browser.storage API,允许存储 kv 数据,可以存储任何可以被结构化克隆的数据,通常而言对于扩展的配置功能足够了。除此之外,有时候还使用 localStorage(如果是 Content Script)或 indexeddb(简单的 kv 存储不足以满足需求时)来存储扩展的设…
阅读全文
13前言 在此之前的基础教程系列中,为了演示创建了多个 Chrome 扩展程序,它们通常都比较简单。所以在本章,我将说明扩展之外另一种允许用户修改网页的标准:User Script,它的基本思路非常简单,向用户浏览的网站注入自定义的一个脚本片段。但与扩展不同,使用 User Script 通常需要安装一个脚本管理器,例如 Tampermonkey:适用性广泛,支持 Chrome/Firefox/Safari,但并未开源 https://www.tampermonkey.net/ Violentmonkey:Tampermonkey 开源替代品,但不支持 Chrome MV3,事实上放弃了最大的用户群 https://violentmon…
阅读全文
14前言 在上一章 Browser Extension Dev - 03. 注入 UI 中,我介绍了如何向网页中注入自定义的 UI,同时还了解了如何使用 Shadow DOM、Tailwind CSS 和使用 npm 包。在本章,我将介绍 Background Script,这是扩展的核心元素之一。 首先,什么是 Background Script? 顾名思义,这是扩展可以在后台运行的脚本,与注入到网页的脚本有所不同,它有几个显著的特点: 可以访问所有扩展 API,在扩展的其他部分,例如 Content Script,可以访问的扩展 API 极其受限,例如无法访问 tabs API 来获取当前浏览器所有打开的标签页。 全局唯一,对于 C…
阅读全文
15背景 最近因为多邻国 App 的 Bug,我正在考虑切换到 Web 版本。但没想到移动端 Web 版本体验那么差,每次打开首页都会弹出 App 下载推广,这真的太烦人了。我第一时间就想去写个 UserScript 彻底删除它,但没想到还碰到一些有趣的问题。 分析 从 DevTools > Elements 中很容易就找到了弹窗元素的稳定选择器。 1 #overlays:has([href*="adj.st"]) 页面遮罩也很容易定位。 1 [data-test="drawer-backdrop"] 然后我就可以正常点击了,但我仍然无法自由滚动,而滚动条锁定又不在 HTML/body 上,所以这种情况下应该如何找到是哪个 HTM…
阅读全文
16前言 在此之前,通过 Browser Extension Dev - 02. 使用 WXT 我们已经了解了扩展的基本结构和 WXT 的使用,下面我们将进一步演示如何在网页中注入复杂的 UI,使用 React、Tailwind CSS、shadcn、或任何需要的 npm 包是怎么做的。 你可能有几个问题 如果网站也使用了 Tailwind CSS,扩展重复使用不会导致样式冲突吗? React 之类的现代 Web 框架如何注入到现有的网站中? 下面我将通过实现一个 Youtube 视频截图扩展来进行演示。 思考 首先,需要明确期望的扩展 UI & UX 是什么样的 扩展的图标自动注入到视频右下角的工具栏中 点击图标自动截取视频当…
阅读全文
17什么是 WXT? 在第一章 Browser Extension Dev - 1. 介绍基本概念 里面,我使用了原始的 JavaScript 实现了一个简单的 Chrome 扩展。现在,我将使用 WXT 重写扩展。那么问题是:什么是 WXT? 简单来说,WXT 是用于浏览器扩展的开发框架,就像 Vite 是用于 Web 开发的流行框架一样。实际上,WXT 是基于 Vite 实现的,所以它也可以使用 Vite 插件的生态系统。在我看来,这是个非常棒的决定。 WXT 解决了什么问题? 那么,它能解决什么问题呢? 支持使用 TypeScript、Npm、React 等现代前端开发工具 – 对于熟悉现代 Web 工具链的开发者而言会感到宾至如…
阅读全文
18你是否曾经对某个网页的功能感到不满?比如 Google 搜索页面上那个显眼的 AI Mode 按钮: 通过浏览器扩展,你可以让它彻底消失: 什么是浏览器扩展? 一般而言,浏览器扩展是一种修改用户浏览网页的方式,它赋予了网站使用者而不是开发者更多的权限。让有能力的用户可以自定义它们的网页浏览体验,现代浏览器提供了极其丰富的扩展 API,甚至能实现一些看起来需要 app 才能做到的事情,但它的基本出发点是让用户可以控制正在浏览的网页。 一些例子 隐藏 Google AI 相关功能 根据规则自定义网页重定向 解除网页上的复制粘贴限制 在浏览器后台运行定时任务 基本结构 1 2 3 4 5 6 manifest.json # 扩展入口文件…
阅读全文
19背景 在为 Chrome 开发了一个扩展程序之后,接下来就是移植到其他浏览器中了,而 Firefox 一般认为是首要选择,它们都使用类似的 Browser Extension API 接口,所以这应该非常简单,对吧? 不,Firefox 有很多微妙的长期问题,如果你遇到了,可能会变得非常棘手,下面是一些吾辈在移植扩展到 Firefox 中已经发现的问题。 CSP 问题 CSP 在 Firefox 扩展中很奇怪,与 Chrome 甚至 Safari 都非常不同,例如 Firefox Extension 不支持访问 localhost Firefox Extension 不支持访问 localhost 导致 wxt 这种扩展开发框架无法…
阅读全文
20背景 今年以来,吾辈开始发布一些 Safari 扩展程序到 AppStore 中,由于吾辈并不使用 iPhone,所以仅发布了 Mac 版本。而这个月吾辈开始实践全平台浏览器扩展的开发,即为所有主流的桌面浏览器(Chrome/Safari/Edge/Firefox)和所有支持扩展的移动端浏览器(Kiwi/Edge/Safari/Firefox)发布相同的插件,这让吾辈将发布 iOS Safari 扩展重新提上日程。 关于如何转换 Chrome 扩展为 Safari 扩展,请参考 转换 Chrome Extension 为 Safari 版本 过程 在已经有一个 Mac Safari 扩展的情况下,为它发布到 iOS 版本理论上很简单…
阅读全文