Highlight
Safari 17 继续支持 Manifest v2 和 v3,同时 Web 扩展现已覆盖 xrOS;内容拦截器新增 :has() 选择器,Declarative Net Request 支持修改请求头,App 扩展引入按站点权限,扩展在 Private Browsing 和 Profiles 中的行为更加可控。
核心内容
Web 扩展的跨平台统一
Safari 扩展有四种类型:内容拦截器(Content Blockers)、分享扩展(Share Extensions)、App 扩展(App Extensions)和 Web 扩展(Web Extensions)。Safari 17 继续支持全部四种,但 Apple 明确表示 Web 扩展是浏览器定制的未来方向。
Apple 在 W3C WebExtensions Community Group 中担任联合主席,与其他浏览器厂商共同推动标准化。这意味着用一套代码写的 Web 扩展可以在 iOS、iPadOS、macOS 和 xrOS 上运行。
xrOS 上的 Web 扩展与 iOS 版能力一致:支持脚本注入、后台内容运行和弹出窗口(popovers)。
(00:45)
Safari 17 同时支持 Manifest v2 和 v3,但新功能会持续添加到 v3。建议开发者在合适的时机迁移到 v3。
内容拦截器增强
内容拦截器用 JSON 规则声明式地拦截或隐藏内容,不需要访问用户访问了哪些网站的信息。今年新增了 :has() 选择器支持。
:has() 允许基于子元素匹配父元素。比如隐藏所有包含 .paid-promo 子元素的 .post 元素,这在以前很难用纯 CSS 选择器实现。
(02:50)
Declarative Net Request 更新
Declarative Net Request 让 Web 扩展以声明式方式拦截和修改网络请求,Safari 在底层处理规则执行,省电且保护隐私。
Safari 16.4 起支持修改请求头。你可以设置、添加、删除 HTTP 请求头的值,甚至完全移除某个头。
使用这个功能需要在 manifest 中声明 declarativeNetRequestWithHostAccess 权限,且扩展必须获得按站点授权才能生效。
(03:37)
新增 declarativeNetRequest.setExtensionActionOptions API,可以将 badge 文本设置为自动显示操作计数(如拦截的请求数量)。这让用户能直观看到扩展的工作效果,同时保持浏览历史的私密性。
(05:09)
脚本和存储 API
registerContentScript 系列 API 允许程序化地注册、更新和删除内容脚本。这与 manifest 中静态声明的内容脚本互补,可以按特定页面或条件动态注入脚本,且注册会跨会话持久化。
(05:45)
Safari 16.4 新增了 session storage 区域。与 local storage 不同,session storage 数据存在内存中,Safari 退出时清除。这适合存储敏感数据如解密密钥和认证令牌。
(06:22)
按站点权限
Safari App 扩展现在支持按站点授权,与 Web 扩展的权限模型统一。扩展首次开启时没有任何站点访问权限。当扩展尝试访问页面时,Safari 会在工具栏图标上显示提示。用户点击后可以选择”允许一天”或”始终允许”。
已开启的扩展在获得权限后,工具栏图标会着色显示。权限可以随时撤销。
从旧版本 Safari 升级的用户,现有扩展的权限会被迁移。用户也会看到一个横幅,可以选择”为每个网站询问”来提升隐私保护。
(07:24)
所有扩展现在默认显示工具栏图标。建议提供 PDF 矢量图标,以便正确着色。
Private Browsing 和 Profiles
Safari 17 允许用户单独控制哪些扩展可以访问 Private Browsing 窗口,不需要在其他浏览场景中关闭扩展。
注入脚本或读取页面信息的扩展默认在 Private Browsing 中关闭。内容拦截器等不访问页面内容的扩展则自动允许。
Profiles 是 Safari 今年的新功能,用于隔离浏览数据(历史、Cookie、网站数据)。每个 Profile 可以有独立的扩展设置。扩展在某个 Profile 中开启时,是一个全新的实例,拥有不同的 UUID、后台页面和存储。但按站点权限在所有 Profile 间共享。
(09:02)
如果扩展与原生宿主 App 通信,需要处理来自多个 Profile 的消息。在 beginRequest(with context:) 中解码 userInfo,如果扩展在 Profile 中运行,会有 SFExtensionProfileKey 对应的 profileIdentifier 值。
Safari 17 的 Develop 菜单中,Web Extension Background Content 菜单项会按扩展分组列出后台页面和 service worker,每个扩展下按 Profile 列出可检查的内容。
详细内容
内容拦截器的 :has() 规则
内容拦截器规则使用 JSON 格式定义。:has() 选择器让基于子元素的匹配成为可能。
[
{
"action": {
"type": "hide"
},
"trigger": {
"url-filter": ".*",
"if-domain": ["example.com"],
"selector": ".post:has(.paid-promo)"
}
}
]
关键点:
"type": "hide"隐藏匹配的元素,不阻止网络请求"url-filter": ".*"匹配所有 URL"if-domain"限制规则只在指定域名生效"selector": ".post:has(.paid-promo)"匹配包含.paid-promo子元素的.post元素:has()支持嵌套和组合其他选择器
Declarative Net Request 修改请求头
在 manifest.json 中声明权限:
{
"manifest_version": 3,
"permissions": [
"declarativeNetRequest",
"declarativeNetRequestWithHostAccess"
],
"host_permissions": [
"*://example.com/*"
]
}
关键点:
declarativeNetRequestWithHostAccess是修改请求头和重定向必需的权限host_permissions声明扩展可以访问的域名- 用户必须按站点授予权限后规则才会生效
规则定义(rules.json):
[
{
"id": 1,
"priority": 1,
"action": {
"type": "modifyHeaders",
"requestHeaders": [
{
"header": "User-Agent",
"operation": "set",
"value": "MyExtension/1.0"
},
{
"header": "X-Custom-Header",
"operation": "append",
"value": "extra-value"
},
{
"header": "X-Tracking-Id",
"operation": "remove"
}
]
},
"condition": {
"urlFilter": "||example.com",
"resourceTypes": ["main_frame"]
}
}
]
关键点:
"operation": "set"替换现有头的值"operation": "append"在现有值后追加"operation": "remove"完全移除该头"resourceTypes"限制规则生效的资源类型
Badge 计数设置(background script):
// Safari 16.4+
chrome.declarativeNetRequest.setExtensionActionOptions({
displayActionCountAsBadgeText: true
});
关键点:
displayActionCountAsBadgeText: true自动将操作计数显示为 badge 文本- 目前这是该 API 唯一的选项
- 计数基于规则触发的次数自动更新
程序化内容脚本注册
// 注册一个动态内容脚本
await chrome.scripting.registerContentScripts([
{
id: "webkit-highlight",
matches: ["https://webkit.org/*"],
js: ["highlight.js"],
runAt: "document_idle",
persistAcrossSessions: true
}
]);
// 更新已有脚本
await chrome.scripting.updateContentScripts([
{
id: "webkit-highlight",
matches: ["https://webkit.org/*", "https://bugs.webkit.org/*"]
}
]);
// 删除脚本
await chrome.scripting.unregisterContentScripts({ ids: ["webkit-highlight"] });
关键点:
registerContentScripts动态注册内容脚本persistAcrossSessions: true使注册跨会话持久化updateContentScripts修改已有脚本的配置unregisterContentScripts按 ID 删除脚本- 与 manifest 中静态声明的
content_scripts互补
Session Storage 使用
// 存储敏感数据到 session storage
await chrome.storage.session.set({
authToken: "eyJhbGciOiJIUzI1NiIs...",
decryptionKey: "base64-encoded-key"
});
// 读取数据
const data = await chrome.storage.session.get(["authToken"]);
console.log(data.authToken);
// 与 local storage 对比
await chrome.storage.local.set({ settings: { theme: "dark" } }); // 持久化到磁盘
await chrome.storage.session.set({ tempKey: "value" }); // 内存中,退出清除
关键点:
chrome.storage.session是 Safari 16.4 新增的存储区域- 数据存储在内存中,不写入磁盘
- Safari 退出时自动清除
- 适合存储认证令牌、解密密钥等敏感数据
- API 与
local和sync存储区域一致
SVG 图标配置
{
"manifest_version": 3,
"icons": {
"16": "icon.svg",
"32": "icon.svg",
"48": "icon.svg",
"128": "icon.svg"
}
}
关键点:
- Safari 16.4 起支持 SVG 图标
- 一个 SVG 文件可以替代所有尺寸的 PNG
- Safari 自动处理缩放,保持清晰
- 建议提供矢量图标以便正确着色
Profile 感知的消息处理
import SafariServices
class ExtensionRequestHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
guard let item = context.inputItems.first as? NSExtensionItem,
let userInfo = item.userInfo else { return }
// 检查是否在 Profile 中运行
if let profileId = userInfo[SFExtensionProfileKey] as? String {
// 扩展在某个 Profile 中运行
// 根据 profileId 隔离数据
handleRequest(forProfile: profileId, context: context)
} else {
// 默认浏览模式
handleRequest(context: context)
}
}
}
关键点:
SFExtensionProfileKey标识扩展运行的 ProfileprofileIdentifier是每个 Profile 的唯一标识- 每个 Profile 中的扩展实例有独立的 UUID、后台页面和存储
- 原生宿主 App 需要根据
profileIdentifier隔离数据
核心启发
1. 用 :has() 选择器做精准内容拦截
- 做什么:在内容拦截器规则中使用
:has()选择器,基于页面结构而非 URL 来隐藏特定内容 - 为什么值得做:可以精准拦截”包含广告标签的文章卡片”这类复杂场景,减少误拦截
- 怎么开始:分析目标网站的 DOM 结构,找出广告/干扰内容的父元素特征,编写
:has()规则测试效果
2. 用 Declarative Net Request 做请求头管理
- 做什么:用 Declarative Net Request 修改或清理 HTTP 请求头,比如统一 User-Agent、移除追踪头
- 为什么值得做:声明式处理比拦截所有网络请求再修改更省电,且不需要访问页面内容
- 怎么开始:在 manifest v3 中声明
declarativeNetRequestWithHostAccess权限,编写 modifyHeaders 规则,测试按站点授权流程
3. 用 Session Storage 替代敏感数据的 localStorage
- 做什么:将认证令牌、临时解密密钥等敏感数据从 localStorage 迁移到 sessionStorage
- 为什么值得做:数据不写入磁盘,Safari 退出自动清除,降低敏感信息泄漏风险
- 怎么开始:将
chrome.storage.local.set/get中涉及敏感数据的地方替换为chrome.storage.session.set/get
4. 适配 Private Browsing 和 Profiles
- 做什么:测试扩展在 Private Browsing 和多个 Profile 中的行为,确保数据正确隔离
- 为什么值得做:Safari 17 用户可能同时使用工作 Profile 和个人 Profile,扩展需要在不同场景下独立运行
- 怎么开始:在 Safari 设置中创建多个 Profile,在每个 Profile 中独立安装和测试扩展。检查原生宿主 App 是否正确处理
SFExtensionProfileKey
5. 用程序化脚本注册实现动态功能
- 做什么:根据用户设置或当前页面动态注册和注销内容脚本
- 为什么值得做:不需要为所有页面都注入脚本,只在需要时加载,减少性能开销
- 怎么开始:在扩展选项页中提供功能开关,用户开启时调用
registerContentScripts,关闭时调用unregisterContentScripts
关联 Session
- What’s new in Web Inspector — Safari Web Inspector 的新调试功能
- Rediscover Safari developer features — Safari 开发者功能的全面更新
- What’s new in CSS — CSS 新特性,包括容器查询和层叠层
- Meet Safari for spatial computing — Safari 在 xrOS 上的体验
- What’s new in WebKit — WebKit 引擎的新功能和改进
评论
GitHub Issues · utterances