WWDC Quick Look 💓 By SwiftGGTeam
What’s new in Safari Web Extensions

What’s new in Safari Web Extensions

观看原视频

Highlight

这场 Session 聚焦 Safari 16 对 Web Extensions 的改进,最大主题是 Manifest V3 的迁移支持。如果你有 Chrome/Firefox 扩展并且已经在做 MV3 迁移,Safari 的适配工作会轻松很多。


核心内容

Safari Web Extensions 的难点通常不在第一个版本,而在持续兼容。

一个扩展要同时服务 Mac、iPhone、iPad,还要跟上 Chrome、Firefox、Edge 的扩展平台变化。Manifest V3 改了后台模型,改了脚本注入 API,也收紧了资源暴露和内容安全策略。浏览器之间如果差异太大,维护成本会很快上升。

这场 session 的核心信息是:Safari 15.4 起支持 Manifest V3 扩展,Safari 16 又补齐了一组 Web Extensions API,并提供跨设备同步体验。已有 Manifest V2 扩展仍然受支持;准备迁移的扩展可以逐步采用 service worker、scriptingdeclarativeNetRequestexternally_connectableunlimitedStorage

详细内容

Manifest V3:后台页面变成事件驱动

01:02)Manifest V3 是 Web Extensions 平台的下一版。Safari 15.4 及以后可以运行 Manifest V3 扩展。Apple 同时说明,Safari 会继续支持 Manifest V2 扩展。

Manifest V3 的关键变化之一,是扩展可以用 service worker 替代 background page。service worker 是事件驱动页面,使用 addEventListener 注册监听器。Safari 也允许继续使用 background page,但必须是 non-persistent。

迁移时,最小的第一步是改 manifest 结构。

{
  "manifest_version": 3,
  "background": {
    "service_worker": "background.js"
  },
  "action": {
    "default_icon": {
      "16": "Images/icon16.png"
    },
    "default_title": "defaultTitle"
  }
}

关键点:

  • manifest_version 改成 3,扩展进入 Manifest V3 规则。
  • background.service_worker 指向后台脚本,让后台逻辑按事件启动。
  • action 合并了 Manifest V2 里的 browser_actionpage_action
  • default_icondefault_title 继续描述工具栏按钮的默认展示。

scripting:脚本注入不再写在字符串里

02:03)Manifest V3 把执行 JavaScript 和插入样式的能力从 tabs API 移到新的 scripting API。tabs.executeScript 在 Manifest V3 中不可用,scripting 同时可用于 Manifest V2 和 V3。

旧写法只能把代码塞进字符串。

// Manifest version 2
browser.tabs.executeScript(1, {
  frameId: 1,
  code: "document.body.style.background = 'blue';"
});

关键点:

  • browser.tabs.executeScript 是旧 API。
  • 第一个参数指定 tab。
  • frameId 只能指定一个 frame。
  • code 传入字符串,复杂逻辑很难维护。

新写法可以传函数、参数和目标 frame。

// Manifest version 3
function changeBackgroundColor(color) {
  document.body.style.background = color;
};

browser.scripting.executeScript({
  target: { tabId: 1, frameIds: [ 1 ] },
  func: changeBackgroundColor,
  args: [ "blue" ]
});

关键点:

  • changeBackgroundColor 是普通函数,不再写成字符串。
  • target.tabId 是必填信息;没有 tab ID 会返回错误。
  • target.frameIds 可以指定多个 frame。
  • func 指向要注入的函数。
  • args 把参数传给注入函数。

脚本文件也可以一次注入多个。

// Manifest version 3
browser.scripting.executeScript({
  target: { tabId: 1 },
  files: [ "file.js", "file2.js" ]
});

关键点:

  • target.tabId 指定脚本运行的页面。
  • files 接受数组,适合把复杂逻辑拆成多个文件。
  • 这替代了 tabs.executeScript 一次只能指定一个文件的旧限制。

样式也走同一组 API。

// Add styling
browser.scripting.insertCSS({
  target: { tabId: 1, frameIds: [ 1, 2, 3 ] },
  files: [ "file.css", "file2.css" ]
});

// Remove styling
browser.scripting.removeCSS({
  target: { tabId: 1, frameIds: [ 1, 2, 3 ] },
  files: [ "file.css", "file2.css" ]
});

关键点:

  • insertCSS 注入样式文件。
  • removeCSS 移除已注入的样式文件。
  • frameIds 允许一次处理多个 frame。
  • files 让样式拆分保持清晰。

Session 里的 Sea Creator 示例展示了真实迁移步骤:把 manifest 改到版本 3,把 browser_action 改为 action,再把 browser.tabs.executeScript 改为 browser.scripting.executeScript。第一次运行失败时,控制台报 browser.tabs.executeScript is undefined,原因正是旧 API 在 Manifest V3 中不可用。

web_accessible_resources:资源只开放给指定站点

04:43)Manifest V2 的 web_accessible_resources 只声明文件数组。问题是,只要扩展有访问权限,网页就可能访问这些资源。

Manifest V3 改成资源和匹配规则绑定。

// Manifest version 3
"web_accessible_resources": [
    {
      "resources": [ "pie.png" ],
      "matches": [ "*://*.apple.com/*" ]
    },
    {
      "resources": [ "cookie.png" ],
      "matches": [ "*://*.webkit.org/*" ]
    }
]

关键点:

  • resources 写明要开放的扩展资源。
  • matches 写明哪些网页 URL 可以访问这些资源。
  • pie.png 只开放给 apple.com URL。
  • cookie.png 只开放给 webkit.org 页面。

内容安全策略也改成对象形式。

// Manifest version 3

"content_security_policy" : { "extension_pages" : "script-src 'unsafe-eval' 'self'" }

关键点:

  • Manifest V3 用对象声明 content_security_policy
  • extension_pages 是扩展页面的策略键。
  • 远程脚本源在 Manifest V3 中不再允许。

declarativeNetRequest:把请求修改交给 Safari

10:03declarativeNetRequest 是内容拦截 API。扩展只声明规则集,拦截和修改网络请求的工作由 Safari 执行。

规则集可以写在 manifest 里。

// manifest.json

"permissions": [ "declarativeNetRequest" ],

"declarative_net_request": {
  "rule_resources": [
    {
      "id": "my_ruleset",
      "enabled": true,
      "path": "rules.json"
    }
  ]
}

关键点:

  • permissions 里声明 declarativeNetRequest 权限。
  • declarative_net_request.rule_resources 列出规则集。
  • id 是规则集标识。
  • enabled 控制规则集是否启用。
  • path 指向实际规则文件。

Safari 现在允许 manifest 中声明最多 50 个规则集;同一时间最多启用 10 个。对于有多个过滤模式的扩展,这让规则组织更灵活。

11:13)今年还新增了两个动态更新 API。

// Rules that won't persist

browser.declarativeNetRequest.updateSessionRules({ addRules: [ rule ] });

// Rules that will persist

browser.declarativeNetRequest.updateDynamicRules({ addRules: [ rule ] });

关键点:

  • updateSessionRules 添加或移除当前会话规则。
  • session rules 不会跨浏览器会话或扩展更新保留。
  • updateDynamicRules 更新会保留的规则。
  • dynamic rules 可以更新拦截规则,而不用发布整个扩展更新。

演示场景很具体:Sea Creator 先用规则阻止所有 URL 的图片,再用 updateSessionRules 允许 webkit.org/blog-files 页面加载图片。结果是 WebKit 博客图片恢复显示,Wikipedia 页面图片仍然被拦截。

externally_connectable:网页可以和扩展通信

14:17externally_connectable 允许网站在用户启用扩展时创建自定义行为。使用前,需要在 manifest 中声明 match patterns,决定哪些页面可以和扩展通信。

Apple 特别说明了两个条件:这个能力只使用 browser namespace;用户必须先授予扩展访问页面的权限,扩展才能收发消息。

网页侧发送消息。

// In the webpage
let extensionID = "com.apple.Sea-Creator.Extension (GJT7Q2TVD9)";

browser.runtime.sendMessage(extensionID, { greeting: "Hello!" },
 function(response) {
    console.log("Received response from the background page:");
    console.log(response.farewell);
});

关键点:

  • extensionID 使用扩展 bundle identifier 和 team identifier 组合。
  • browser.runtime.sendMessage 向扩展发送消息。
  • 第二个参数是消息体。
  • 回调函数处理扩展返回的响应。

扩展后台页面监听外部消息。

// In the background page
browser.runtime.onMessageExternal.addListener(function(message, sender, sendResponse) {
    console.log("Received message from the sender:");
    console.log(message.greeting);
    sendResponse({ farewell: "Goodbye!" });
});

关键点:

  • onMessageExternal 接收网页发来的外部消息。
  • message 是网页传入的数据。
  • sender 描述消息来源。
  • sendResponse 把响应发回网页。

跨浏览器发布时,同一个扩展可能有多个商店标识。网页要先确认用户安装的是 Safari Web Extension。

// Determining the correct identifier

function determineExtensionID(extensionID) {
  return new Promise((resolve) => {
    try {
      browser.runtime.sendMessage(extensionID, { action: 'determineID' }, function(response) {
        if (response)
          resolve({ extensionID: extensionID, isInstalled: true, response: response });
        else 
          resolve({ extensionID: extensionID, isInstalled: false });
      });
    }
  });
};

关键点:

  • determineExtensionID 对单个候选 ID 发探测消息。
  • browser.runtime.sendMessage 发送 determineID 动作。
  • 有响应时返回 isInstalled: true
  • 没有响应时返回 isInstalled: false
  • Session 建议有多个 ID 时配合 Promise.all 广播探测,再从结果中找出已安装扩展。

扩展侧需要回应探测消息。

// background.js

browser.runtime.onMessageExternal.addListener(function(message, sender, sendResponse) {
  if (message.action == "determineID") {
    sendResponse({ "Installed" });
  }
});

关键点:

  • 后台脚本监听外部消息。
  • 只处理 actiondetermineID 的消息。
  • sendResponse 告诉网页扩展已安装。

unlimitedStorage:存储配额不再是 10 MB

17:28)Safari 的 unlimitedStorage 现在是真正的 unlimited。扩展不再受 10 MB quota 限制。

启用方式很短。

// manifest.json

"permissions": [ "storage", "unlimitedStorage" ]

关键点:

  • storage 开启扩展存储 API。
  • unlimitedStorage 移除 10 MB 配额限制。
  • 用户仍然可以随时清除扩展使用的数据。
  • Apple 建议只存必要数据,避免用户主动清理。

Safari 16:扩展跨设备同步

18:19)Safari 16 改进了扩展跨设备体验。用户在一台设备上打开扩展后,其他设备的 Extension Settings 会提示下载;下载后会自动启用。

要让这个体验生效,Apple 建议提交 App Store 时同时列出 iOS、iPadOS 和 macOS。同步有两种设置方式。

第一种是 universal purchase。用户只购买一次,跨平台使用同一扩展。设置时,需要让扩展使用单个 bundle identifier,使它关联到 App Store Connect 中同一个 app record。

第二种是手动链接 app。在 Xcode 的 Info.plist 中,为 iOS app、iOS extension、macOS app、macOS extension 添加彼此对应的 bundle identifier。Session 的演示就是在每个 target 的设置里填入对应平台的 app 和 extension 标识。

核心启发

1. 做一个 Manifest V3 迁移检查器

  • 做什么:扫描扩展源码,列出 tabs.executeScriptbrowser_action、Manifest V2 content_security_policy 等迁移点。
  • 为什么值得做:Session 中 Sea Creator 的失败来自 browser.tabs.executeScript is undefined,这类问题可以在构建前发现。
  • 怎么开始:从 manifest.json 解析 manifest_versionbrowser_actionpage_actioncontent_security_policy,再扫描脚本里的 browser.tabs.executeScript,给出对应的 scripting.executeScript 修改建议。

2. 给内容拦截扩展增加站点级开关

  • 做什么:让用户在 popup 中临时允许某个站点加载图片、脚本或请求。
  • 为什么值得做updateSessionRules 可以添加不持久化规则,适合“本次会话放行这个网站”。
  • 怎么开始:在 manifest 中配置 declarativeNetRequest,默认规则放在 rules.json,用户点击放行时调用 browser.declarativeNetRequest.updateSessionRules({ addRules: [rule] })

3. 为网站提供“检测扩展是否安装”的入口

  • 做什么:网站在用户访问时检测 Safari Web Extension 是否存在,存在时显示扩展专属功能。
  • 为什么值得做externally_connectable 允许网页和扩展通信,适合网站与扩展配合完成登录、保存、标注、内容增强等流程。
  • 怎么开始:网页侧用 browser.runtime.sendMessage(extensionID, { action: 'determineID' }) 探测,扩展后台用 browser.runtime.onMessageExternal.addListener 回复。

4. 给跨设备扩展做发布前清单

  • 做什么:检查 iOS、iPadOS、macOS 的 app 和 extension 是否具备跨设备下载与启用所需配置。
  • 为什么值得做:Safari 16 会把用户已启用的扩展带到其他设备,但前提是 App Store 和 bundle identifier 配置正确。
  • 怎么开始:优先采用 universal purchase;如果不能采用,就在 Xcode 的 Info.plist 中为 app 和 extension 填入对应平台的 bundle identifiers。

5. 把大数据扩展改成“可清理”的存储模型

  • 做什么:为密码管理、网页剪藏、阅读标注等扩展设计存储占用面板和清理按钮。
  • 为什么值得做unlimitedStorage 取消 10 MB quota,但用户仍然可以清除扩展数据。
  • 怎么开始:manifest 声明 "permissions": [ "storage", "unlimitedStorage" ],存储界面只保留必要数据,并显示缓存、索引和用户内容的占用。

关联 Session

评论

GitHub Issues · utterances