创建 Safari 浏览器的网页扩展
Create web extensions for Safari
2026年6月10日
一句话判断
这场 Session 最值得关注的一件事是:Safari 扩展开发终于把前端开发者当人看了,全程无需打开 Xcode 就能完成 90% 的开发和调试。
这场 Session 讲了什么
以前想把 Chrome 扩展搬到 Safari,或者从零写一个 Safari 扩展,你得先建个 Xcode 工程,配一堆 Target,对纯前端同学来说简直是折磨。现在 Apple 彻底想通了,全面拥抱 W3C Web Extensions 标准。你只需要一个顺手的代码编辑器(比如 VS Code),写个 manifest.json,配上 HTML/CSS/JS,就能在 Safari 里直接加载未签名的扩展进行调试。
整个流程变成了标准的 Web 开发体验:改代码、保存、在 Safari 设置里点一下重载,立刻看效果。直到你要上 App Store 或者用 TestFlight 发内测,才需要请出 Xcode 的 Safari Web Extension Packager 工具打个包。这对那些手里已经有成熟 Chrome/Firefox 扩展团队来说,接入 Safari 的成本直接从“重构”降到了“改改配置”。
值得深挖的点
Xcode 解绑与 W3C 标准的全面妥协
Apple 以前搞 Safari App Extensions,非要把它和 macOS/iOS 原生 App 强绑定,导致开发门槛极高。这次 Session 明确展示了“无 Xcode 开发”流程,这其实是 Apple 在扩展生态上的一次重大妥协。为什么这么做?因为 Manifest V3 已经是跨浏览器的事实标准,Apple 如果再坚持自己那一套,Safari 扩展生态只会越来越死。
把开发阶段和打包阶段解耦,Trade-off 很明显:开发体验直线飙升,前端同学狂喜;但代价是,在纯 Web 开发阶段,你无法调用任何 Apple 平台的原生 API(比如 Local Authentication 或 Keychain)。Session 里也给出了折中方案:先用纯 Web 方式把核心逻辑(比如 UI 和拦截规则)跑通,最后再用 Xcode 包一层壳,通过 Native Messaging(原生消息传递)把 JS 和 Swift/ObjC 连起来。这种“先 Web 后 Native”的分层开发模式,是现在做跨平台 Safari 扩展的最优解。
declarativeNetRequest 的权限切分与隐私偏执
Session 花了很大篇幅讲内容拦截,用的是 declarativeNetRequest API。这里有个很有意思的设计细节:Apple 把“拦截”和“重定向”的权限拆开了。如果你只是 block 掉某个请求,只需要 declarativeNetRequest 权限;但如果你想把请求 redirect 到自定义页面(比如 Session 里的防沉迷提示页),就必须申请 declarativeNetRequestWithHostAccess。
这种设计背后的逻辑是 Apple 对隐私的偏执。Block 只是丢弃数据,不侵犯隐私;但 Redirect 意味着扩展需要读取并接管原始请求的上下文,风险更高。所以 Apple 强制要求后者必须配合 Host Permissions(主机权限),甚至推荐使用 Optional Host Permissions(可选主机权限),让用户在触发具体功能时再手动授权。相比于 Chrome 以前那种一装扩展就索要 <all_urls> 的粗放模式,Safari 这种“按需申请、最小可用”的权限模型虽然写起来麻烦点,但过审率高,用户信任度也更好。
代码片段
1. 极简 Manifest 与 UI 配置
场景:配置扩展的基础信息,并挂载一个设置页面(Options UI),而不是挤在工具栏的 Popup 里。
{
"manifest_version": 3,
"name": "Shiny OnTrack",
"description": "Stay on track while you browse the web",
"version": 1.0,
"icons": {
"512": "images/icon.svg" // 使用 SVG,Safari 会自动处理多分辨率缩放
},
"options_ui": {
"page": "options.html" // 挂载全屏的设置页面,适合复杂的列表配置
},
"permissions": [ "declarativeNetRequest" ] // 基础拦截权限
}
坑:如果你用 Popup (default_popup) 做复杂的表单或长列表,UI 会非常拥挤且交互反人类,复杂配置老老实实用 options_ui。
2. 动态注入拦截与重定向规则
场景:用户在设置页添加了一个域名,扩展动态生成规则,将其重定向到自定义的“防沉迷”提示页。
// 生成唯一的规则 ID
const ruleId = generateUniqueIdForHost(host);
// 构建重定向规则
const redirectRule = {
id: ruleId,
priority: 1,
action: {
type: "redirect",
redirect: { extensionPath: "/blocked.html" } // 重定向到扩展内的自定义页面
},
condition: {
urlFilter: `||${host}`, // 匹配目标域名
resourceTypes: ["main_frame"] // 仅拦截主文档请求
}
};
// 动态更新规则
// 注意:使用 redirect 必须在 manifest 中声明 declarativeNetRequestWithHostAccess 权限
chrome.declarativeNetRequest.updateDynamicRules({
addRules: [redirectRule],
removeRuleIds: [ruleId] // 避免 ID 冲突
});
坑:redirect 动作比 block 动作需要更高的权限(declarativeNetRequestWithHostAccess),且必须配合对应域名的 Host Permissions 才能生效,否则规则会被 Safari 静默忽略。
最佳实践
- 已有 Chrome 扩展迁移:如果你的扩展已经是 Manifest V3,直接复用 95% 的代码。重点检查
background脚本,Safari 强制要求使用 Service Worker,不支持 persistent background pages。另外,把<all_urls>这种宽泛权限拆成 Optional Host Permissions,在 Safari 里体验会好很多。 - 新项目架构建议:采用“核心逻辑纯 Web + 平台特性 Native”的架构。把 UI、网络拦截、DOM 操作全部用标准 Web API 写好,这部分代码可以跨浏览器复用。只有当需要用到 Touch ID、Keychain 或跨 App 通信时,才在 Xcode 里写一层薄薄的 Native Wrapper,通过
browser.runtime.sendNativeMessage桥接。 - 调试避坑:在 Safari 设置里开启“允许未签名的扩展”后,每次修改代码不需要重启 Safari,只需在扩展管理面板点一下“重新加载”按钮。但如果修改了
manifest.json里的权限,建议彻底关掉 Safari 再重开,避免权限缓存导致灵异事件。
还有什么值得关注
- 原生消息传递 (Native Messaging):Session 展示了通过 Xcode 打包后,JS 可以直接和包含它的 macOS/iOS App 通信,这是解锁 Apple 平台专属能力(如 Local Authentication)的唯一途径。
- TestFlight 无缝分发:Safari 扩展现在可以像普通 App 一样,直接通过 TestFlight 分发给测试用户,终于不用每次内测都到处发
.ipa或.pkg了。 - visionOS 自动支持:只要你的扩展兼容 iOS/iPadOS,它会自动在 visionOS 的 Safari 里运行,无需任何额外适配代码。