Highlight
Safari 现在支持用纯 HTML、CSS、JavaScript 开发 Web 扩展,无需 Xcode 即可本地调试,通过 App Store Connect 可直接打包分发到 iOS、iPadOS、macOS 和 visionOS。
核心内容
以前开发 Safari 扩展,必须先打开 Xcode 建一个 macOS/iOS App,再把扩展嵌进去。对前端开发者来说,这套流程门槛高、反馈慢,很多人干脆放弃。WWDC26 的这场 session 彻底改变了这个局面。
Apple 现在全面拥抱 W3C Web Extensions 标准。你只需要一个代码编辑器,写个 manifest.json,配几行 HTML/CSS/JS,就能在 Safari 里直接加载、调试。整个开发体验跟写 Chrome 扩展几乎没有区别。
Session 里演示了一个真实案例:一个叫 “Shiny OnTrack” 的扩展,帮用户屏蔽分散注意力的网站。它有两种模式:Full 模式直接拦截访问,Light 模式在页面上显示一个 10 分钟倒计时。这个扩展从零搭建到上架 App Store 的完整流程,就是本场 session 的主线。
开发阶段不再需要 Xcode
(03:23)
打开 Safari 设置,勾选 Advanced 面板里的 “Show features for web developers”,就能在 Extensions 标签页加载未签名的本地扩展。选文件夹、允许未签名扩展,三步完成加载。改完代码点一下 Reload,立刻看到效果。
这跟以前必须 Xcode Build & Run 的迭代速度完全不在一个量级。
权限模型:最小可用原则
(07:53)
Safari 的权限设计把用户隐私放在前面。扩展需要什么能力,必须在 manifest.json 里声明。Apple 还推荐用 optional_host_permissions 代替直接索要全站权限,等用户触发具体功能时再请求访问。用户点了允许,扩展图标才会变成激活状态,时刻提醒用户这个页面上有扩展在运行。
一条命令打包上架
(22:49)
开发完成后,一条命令就能生成 Xcode 工程:
xcrun safari-web-extension-packager --copy-resources /path/to/extension
App Store Connect 现在也支持直接上传扩展资源、自动打包,连 Mac 都不需要。上传后走 TestFlight 内测,再提交审核,流程跟普通 App 完全一致。
原生能力通过 Native Messaging 桥接
(23:02)
如果扩展需要调用系统级能力(比如生物识别认证),可以通过 Native Messaging 与包含它的原生 App 通信。JS 发消息给 App Extension,App Extension 转给原生 App,结果再原路返回。Session 里演示了用 Touch ID 保护设置页面的完整代码。
详细内容
Manifest V3 基础配置
(03:44)
每个扩展的起点都是 manifest.json:
{
"manifest_version": 3,
"name": "Shiny OnTrack",
"description": "Stay on track while you browse the web",
"version": 1.0,
"icons": {
"512": "images/icon.svg"
},
"options_ui": {
"page": "options.html"
}
}
关键点:
manifest_version必须为 3,这是 W3C 当前标准- 图标用 SVG,Safari 自动处理多分辨率缩放
options_ui定义设置页面,比 toolbar popup 更适合复杂配置
用 declarativeNetRequest 拦截和重定向请求
(08:18)
内容拦截是扩展最常见的需求。Apple 提供了 declarativeNetRequest API,规则可以静态写在 manifest 里,也可以运行时动态添加。
静态规则适合已知拦截列表:
{
"permissions": ["declarativeNetRequest"],
"declarativeNetRequest": {
"rule_resources": [
{
"id": "ruleset_id",
"enabled": true,
"path": "rules.json"
}
]
}
}
动态规则适合用户自定义场景。Session 里演示了一个 helper,把域名映射成唯一的规则 ID:
export function hostToRuleID(host) {
let hash = 0;
for (let i = 0; i < host.length; i++) {
hash = ((hash << 5) + hash) + host.charCodeAt(i);
hash |= 0;
}
return Math.abs(hash) || 1;
}
function createBlockRule(host) {
return {
id: hostToRuleID(host),
priority: 1,
action: { type: "block" },
condition: {
urlFilter: `||${host}`,
resourceTypes: ["main_frame"]
}
};
}
export async function createRules(hosts) {
try {
await browser.declarativeNetRequest.updateDynamicRules({
addRules: hosts.map(createBlockRule)
});
} catch {
console.log("Failed to create declarative net request rules");
}
}
关键点:
hostToRuleID用字符串哈希生成稳定 ID,同一域名每次得到相同 IDurlFilter: ||${host}匹配该域名及其所有子域名resourceTypes: ["main_frame"]只拦截主文档导航,不碰子资源- 动态规则通过
updateDynamicRules批量增删
重定向到自定义页面
(10:48)
直接 block 会给用户看一个生硬的错误页。改成重定向到扩展内的自定义页面,体验好很多。但重定向需要更高的权限:
{
"permissions": ["declarativeNetRequestWithHostAccess"],
"optional_host_permissions": ["*://*/*"]
}
重定向规则:
function createRedirectRule(host) {
return {
id: hostToRuleID(host),
priority: 1,
action: {
type: "redirect",
redirect: { extensionPath: "/blocked.html" }
},
condition: {
urlFilter: `||${host}`,
resourceTypes: ["main_frame"]
}
};
}
关键点:
declarativeNetRequestWithHostAccess替代普通declarativeNetRequestextensionPath指向扩展包内的本地文件- 必须先获得对应域名的 host permission,重定向才会生效
运行时请求权限
(13:42)
用户添加域名时,扩展动态申请权限:
export async function addHost(host, blockingMode) {
if (!host) return;
const granted = await browser.permissions.request({
origins: [`*://${host}/*`, `*://*.${host}/*`]
});
if (!granted) return;
if (blockingMode === "full")
await createRules([host]);
}
关键点:
permissions.request弹出系统授权对话框,用户点了允许才继续- 被拒绝就直接 return,不执行后续逻辑
- 这种”按需申请”模式比一次性索要
<all_urls>更容易获得用户信任
Content Scripts 注入页面
(14:55)
Light 模式需要在目标页面上显示倒计时,这要用到 Content Scripts:
function contentScript(host) {
return {
id: `cs-${host}`,
js: ["content.js"],
css: ["content.css"],
matches: [`*://${host}/*`, `*://*.${host}/*`],
persistAcrossSessions: true
};
}
export async function registerScripts(hosts) {
const scripts = hosts.map(contentScript);
try {
await browser.scripting.registerContentScripts(scripts);
} catch {
console.log("Failed to register content scripts");
}
}
关键点:
persistAcrossSessions: true保证 Safari 重启后脚本还在- 每个域名分配独立 ID,避免冲突
- 需要
scripting权限
Storage API 持久化数据
(17:06)
扩展重启后数据不能丢。Safari 提供两种存储:
browser.session.storage:内存级,重启清空browser.storage.local:磁盘级,持久保存
export async function updateHosts(hosts) {
await browser.storage.local.set({ hosts: hosts });
}
export async function getHosts() {
const { hosts = [] } = await browser.storage.local.get("hosts");
return hosts;
}
export async function saveBlockMode(mode) {
await browser.storage.local.set({ blockMode: mode });
}
export async function getBlockMode() {
const { blockMode = "full" } = await browser.storage.local.get("blockMode");
return blockMode;
}
关键点:
storage.local写磁盘,适合用户配置和黑名单- 读取时用解构赋默认值,避免首次使用时 undefined
Background Script 处理生命周期
(19:01)
Content Scripts 在扩展更新后会丢失,需要在 background script 里重新注册:
browser.runtime.onInstalled.addListener(async (details) => {
if (details.reason !== "update") return;
const hosts = await getHosts();
await registerScripts(hosts);
});
关键点:
onInstalled在扩展安装或更新时触发- 过滤
reason !== "update"避免重复执行 - 从 storage 读取已保存的域名列表,恢复 content scripts
Native Messaging 调用系统能力
(23:32)
扩展通过 Native Messaging 与包含它的原生 App 通信。JS 端:
export async function requestBioAuth() {
const message = { message: "requestBioAuth" };
const response = await browser.runtime.sendNativeMessage(message);
return response?.success;
}
Swift 端在 SafariWebExtensionHandler 中处理:
import LocalAuthentication
class SafariWebExtensionHandler: NSObject, NSExtensionRequestHandling {
func beginRequest(with context: NSExtensionContext) {
let request = context.inputItems.first as? NSExtensionItem
let message = request?.userInfo?[SFExtensionMessageKey] as? [String: Any]
if message?["message"] as? String == "requestBioAuth" {
let laContext = LAContext()
Task {
do {
let success = try await laContext.evaluatePolicy(
.deviceOwnerAuthenticationWithBiometrics,
localizedReason: "Authenticate to change blocked sites"
)
self.reply(context: context, success: success)
} catch {
self.reply(context: context, success: false)
}
}
}
}
private func reply(context: NSExtensionContext, success: Bool) {
let response = NSExtensionItem()
response.userInfo = [SFExtensionMessageKey: ["success": success]]
context.completeRequest(returningItems: [response], completionHandler: nil)
}
}
关键点:
sendNativeMessage把 JSON 消息发给 App Extension- App Extension 通过
NSExtensionRequestHandling接收消息 - 用
LocalAuthentication框架做生物识别认证 - 结果通过
completeRequest原路返回给 JS
核心启发
1. 做一款跨浏览器的密码管理扩展
Safari 现在用标准 WebExtensions API,意味着 Chrome/Firefox 扩展的代码可以直接复用。把核心逻辑(自动填充、密码生成)用纯 JS 写好,再为 Safari 包一层 App Store 壳子。Native Messaging 可以用来调用 iOS Keychain 做生物识别解锁。
入口:复用现有的 Manifest V3 代码,重点适配 browser.storage.local 和权限模型。
2. 做一款网页内容高亮和笔记工具
Content Scripts 可以读写任意网页的 DOM。用 scripting.registerContentScripts 动态注入高亮逻辑,用 storage.local 保存高亮位置和笔记内容。跨设备同步可以通过 iCloud Key-Value Storage 走 Native Messaging 桥接。
入口:先实现 content.js 中的文本选区捕获和高亮渲染,再接入 storage 持久化。
3. 做一款专注模式扩展
参考 session 里的 “Shiny OnTrack”,但可以做得更智能。比如根据时间自动切换拦截列表(工作时间屏蔽社交网站,晚上屏蔽工作邮箱),或者根据屏幕使用时长动态调整拦截强度。declarativeNetRequest.updateDynamicRules 支持运行时批量更新规则,完全能支撑这种动态策略。
入口:在 options 页面配置多套规则集,background script 根据时间或事件切换规则集。
4. 做一款网页自动化测试辅助工具
Content Scripts 可以注入测试脚本、收集页面性能数据、截屏对比。用 browser.scripting.executeScript 在特定页面临时注入测试代码,结果通过 Native Messaging 传回原生 App 做可视化报告。
入口:用 scripting API 在目标页面注入测试 runner,用 storage.local 缓存测试结果。
5. 把现有的 Chrome 扩展迁移到 Safari
如果你已经有 Manifest V3 的 Chrome 扩展,迁移成本极低。主要检查点:
- 把
chrome.*API 换成browser.*(Safari 同时支持两种命名空间) - 把宽泛的 host permissions 改成
optional_host_permissions - 确认 background script 不是 persistent page(Safari 要求 service worker 或 non-persistent background page)
入口:用 safari-web-extension-packager 一键生成 Xcode 工程,测试通过后直接上架。
关联 Session
- 204 - WebKit — Safari 27 的新 Web 平台特性,扩展开发者需要了解底层 WebKit 支持的变化
- 314 - CSS Grid — 扩展的 UI 页面(options/popup)本质就是网页,CSS Grid 的新能力可以直接用上
- 320 - Immersive web — 如果扩展需要在 visionOS Safari 中提供空间化体验,需要了解沉浸式 Web 标准
- 344 - Code-along: Build a great experience for Siri — 扩展可以通过 App Intents 与 Siri 集成,让用户用语音控制扩展功能
- 370 - TextKit — 如果扩展涉及网页文本处理(如高亮、翻译),TextKit 的新排版能力值得了解
评论
GitHub Issues · utterances