Create web extensions for Safari
Xcode & Developer Tools 入门 26m

创建 Safari 浏览器的网页扩展

Create web extensions for Safari

2026年6月10日

在 Apple 官方观看视频

一句话判断

这场 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 里运行,无需任何额外适配代码。
Safari Web Extensions Manifest V3 declarativeNetRequest Web Development