Meet Safari Web Extensions on iOS
Media & Web 进阶 20m

认识 iOS 上的 Safari Web 扩展

Meet Safari Web Extensions on iOS

2021年6月10日

在 Apple 官方观看视频

一句话判断

Safari Web Extensions 终于登陆 iOS —— 基于 WebExtensions API 标准,Chrome/Firefox 扩展开发者可以快速移植,但 iOS 端的限制(不支持侧边栏、不支持 devtools)让很多桌面端扩展无法直接搬过来。

这场 Session 讲了什么

iOS 15 首次支持 Safari Web Extensions,允许开发者创建可以修改 Safari 浏览行为的扩展。这些扩展基于标准的 WebExtensions API(和 Chrome、Firefox、Edge 使用相同的 API 规范),用 JavaScript、HTML 和 CSS 编写。

Session 讲解了从创建扩展项目到上架 App Store 的完整流程。Xcode 13 提供了 Safari Extension App 模板,生成的项目包含 manifest.json、popup 页面、background script 和 content script 的基本结构。

iOS 端的特殊限制:不支持 sidebar(侧边栏)、不支持 devtools 面板、popup 页面以弹窗形式展示、扩展设置在 Safari 的扩展管理页面中。用户需要在设置中手动启用扩展并授予权限。

还讨论了如何用 native messaging 在扩展和容器 app 之间通信。

值得深挖的点

manifest.json 的跨平台配置。iOS Safari Extension 的 manifest 和桌面端 Chrome Extension 的 manifest 基本兼容,但有几个 iOS 特有的差异:browser_action 替代了 page_action(iOS 没有 toolbar 概念)、background.scripts 在 iOS 上有生命周期限制(后台脚本在 Safari 不活跃时会被挂起)。苹果建议用 browser.commandsbrowser.alarms 替代长驻后台逻辑。

Native Messaging 的桥接机制。iOS 上 Safari Extension 和容器 app 之间的通信通过 browser.runtime.sendNativeMessage() 实现。消息通过 app group 共享的 IPC 通道传递。这意味着你的扩展可以利用 Swift/Objective-C 代码来处理需要原生能力的任务(如加密、文件访问、系统 API 调用)。

代码片段

manifest.json 基础配置

{
    "manifest_version": 2,
    "name": "My iOS Safari Extension",
    "version": "1.0",
    "description": "一个示例 Safari 扩展",
    "icons": {
        "48": "icon-48.png",
        "96": "icon-96.png"
    },
    "browser_action": {
        "default_popup": "popup.html",
        "default_icon": {
            "16": "toolbar-icon-16.png",
            "32": "toolbar-icon-32.png"
        }
    },
    "permissions": [
        "activeTab",
        "storage",
        "nativeMessaging"
    ],
    "background": {
        "scripts": ["background.js"]
    },
    "content_scripts": [
        {
            "matches": ["*://*.example.com/*"],
            "js": ["content.js"],
            "css": ["content.css"]
        }
    ]
}

Content Script 修改网页

// content.js —— 在匹配的网页上运行
(function() {
    // 隐藏广告横幅
    const ads = document.querySelectorAll('.ad-banner, .sponsored-content');
    ads.forEach(ad => ad.style.display = 'none');

    // 注入自定义按钮到页面
    const button = document.createElement('button');
    button.textContent = '保存到 MyApp';
    button.style.cssText = 'position:fixed;bottom:20px;right:20px;z-index:9999;padding:10px 20px;background:#007AFF;color:white;border:none;border-radius:8px;';
    button.addEventListener('click', () => {
        // 获取页面信息
        const pageInfo = {
            title: document.title,
            url: window.location.href,
            selection: window.getSelection().toString()
        };
        // 发送给 background script
        browser.runtime.sendMessage({ type: 'savePage', data: pageInfo });
    });
    document.body.appendChild(button);
})();

Background Script 处理消息

// background.js —— 扩展的后台逻辑
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
    if (message.type === 'savePage') {
        // 通过 native messaging 发送给容器 app
        browser.runtime.sendNativeMessage('com.myapp.safari', {
            action: 'save',
            data: message.data
        }, response => {
            sendResponse({ success: true, savedId: response.id });
        });
        return true;  // 保持消息通道开启(异步响应)
    }
});

// 监听标签页更新
browser.tabs.onUpdated.addListener((tabId, changeInfo, tab) => {
    if (changeInfo.status === 'complete') {
        console.log('页面加载完成:', tab.url);
    }
});

最佳实践

  1. 从 Chrome Web Store 移植时先在 iOS Safari 中测试兼容性。大部分 WebExtensions API 在 iOS 上可用,但 browser.sidebarbrowser.devtools 等完全不支持。用 feature detection 替代浏览器检测。
  2. Native Messaging 的数据量要控制。iOS 上 IPC 通道有大小限制,单条消息不要超过 1MB。大文件传输建议通过 app group 的文件系统。
  3. 扩展的权限请求要最小化。iOS 用户对权限弹窗很敏感,permissions 中只列你真正需要的域和 API。用 activeTab 替代 tabs 权限(只在用户点击扩展时获取当前标签信息)。

还有什么值得关注

  • Safari Web Extensions 在 macOS 12 上也同步更新,支持了新的 Declarative Net Request API。
  • iOS 15 的 Safari 重新设计了扩展管理界面,用户可以在地址栏直接访问扩展功能。
  • browser.storage.sync 在 iOS 上通过 iCloud 在设备间同步扩展数据。
WWDC 2021