View in English

  • Apple 开发者
    • 入门汇总

    探索“入门汇总”

    • 概览
    • 学习
    • Apple Developer Program

    及时了解最新动态

    • 最新动态
    • 开发者你好
    • 平台

    探索“平台”

    • Apple 平台
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    • App Store

    精选

    • 设计
    • 分发
    • 游戏
    • 配件
    • 网页
    • Home
    • CarPlay 车载
    • 技术

    探索“技术”

    • 概览
    • Xcode
    • Swift
    • SwiftUI

    精选

    • 辅助功能
    • App Intents
    • Apple 智能
    • 游戏
    • 机器学习与 AI
    • 安全性
    • Xcode Cloud
    • 社区

    探索“社区”

    • 概览
    • “与 Apple 会面交流”活动
    • 社区主导的活动
    • 开发者论坛
    • 开源

    精选

    • WWDC
    • Swift Student Challenge
    • 开发者故事
    • App Store 大奖
    • Apple 设计大奖
    • Apple Developer Centers
    • 文档

    探索“文档”

    • 文档库
    • 技术概述
    • 示例代码
    • 《人机界面指南》
    • 视频

    发布说明

    • 精选更新
    • iOS
    • iPadOS
    • macOS
    • watchOS
    • visionOS
    • Apple tvOS
    • Xcode
    • 下载

    探索“下载”

    • 所有下载
    • 操作系统
    • 应用程序
    • 设计资源

    精选

    • Xcode
    • TestFlight
    • 字体
    • SF Symbols
    • Icon Composer
    • 支持

    探索“支持”

    • 概览
    • 帮助指南
    • 开发者论坛
    • “反馈助理”
    • 联系我们

    精选

    • 《开发者账户帮助》
    • 《App 审核指南》
    • 《App Store Connect 帮助》
    • 即将实行的要求
    • 协议和准则
    • 系统状态
  • 快速链接

    • 活动
    • 新闻
    • 论坛
    • 示例代码
    • 视频
 

视频

打开菜单 关闭菜单
  • 专题
  • 所有视频
  • 关于

更多视频

  • 简介
  • 概要
  • 转写文稿
  • 代码
  • 创建 Safari 浏览器的网页扩展

    从零开始构建并测试 Safari 浏览器网页扩展,放心迈出入门第一步,而且全程无需使用 Xcode。探索内容拦截、页面修改、原生通信和权限模式如何协同工作,跨平台打造强大且保护隐私的浏览体验。

    章节

    • 0:00 - Introduction
    • 3:23 - Get started
    • 7:23 - Block content
    • 14:40 - Modify webpages
    • 19:53 - Package and distribute
    • 22:33 - Communicate with your app
    • 26:04 - Next steps

    资源

    • w3.org — W3C WebExtensions Community Group
    • Packaging and distributing Safari Web Extensions with App Store Connect
    • WebKit.org – Report issues to the WebKit open-source project
    • Submit feedback
    • MDN Web Docs - Web Extensions API
      • 高清视频
      • 标清视频

    相关视频

    WWDC26

    • 面向 Safari 浏览器 27 的 WebKit 新功能
  • 搜索此视频…

    你好!

    我是Kiara 一名来自 Safari团队的工程师 如果你曾有过想在Safari 中看到的功能想法 并且想 将这个想法变为现实 这节课就是为你准备的 我将带你了解构建和分发 所需了解的一切 Safari网页扩展的全部知识 内容很多 你可以随时休息 或跳到最相关的章节 对你最有用的部分 Safari网页扩展 封装在一个App中 说实话 这是我最喜欢 从App Store获取的东西之一 无论是屏蔽广告 还是构建自定义新标签页 还是在你最喜爱的 流媒体平台上提升播放体验 它们虽小 却能切实改善 你浏览网页的体验 Apple正在W3C网页扩展 工作组中与其他浏览器合作 以标准化跨浏览器 构建网页扩展所用的API 因此如果你已为 其他浏览器开发了扩展 就可以将扩展迁移到Safari 跳转到打包与分发章节 我将展示如何 使用App Store Connect 发布你的扩展 今天 我将全面讲解 如何从零开始构建 一个网页扩展 我将带你了解扩展的 开发流程并重点介绍 可用于在Safari中带来 可定制体验的关键API和功能 我还将展示如何在Safari中 测试网页扩展 以及如何使用 TestFlight与用户 分享扩展的Beta版本 一旦我准备好与世界分享 我的作品 我将提交扩展 到App Store 首先 下载本节课的 示例代码项目 其中包含我今天将要 构建的扩展的所有资源 这样你就可以跟着一起做了 在本节课中 我将带你 构建一个真实的扩展 一个能屏蔽内容 并修改网页的扩展 然后 我将介绍几种不同的 打包方式供你选择 并将扩展发布到App Store 为了让扩展功能更强大 我将展示如何 让扩展与包含它的App 协同工作 最终 这个扩展将同时在 iOS iPadOS macOS上的Safari运行 以及visionOS上 因为网页扩展的魅力在于 它们全都由 HTML CSS和JavaScript组成 所以如果你之前做过 任何Web开发 你已经掌握了大部分所需知识 在今天的课程中 我将构建 一个允许用户屏蔽 分散注意力的网站 让他们专心浏览网页的扩展 如你所知 浏览时有很多 令人分心的"兔子洞" 以webkit.org为例 那里有数百篇文章 我很容易就会花上好几个小时 只是阅读WebKit的新内容 所以 我需要这样一个扩展 今天我要构建它 它有 两种不同的屏蔽模式 一种轻量模式 允许在某个 网站上浏览最多10分钟 非常适合阅读几篇WebKit文章 还有一种完全模式 会在用户尝试时立即重定向 跳转到该网站 首先 我打开 我最喜欢的代码编辑器 你也可以使用Xcode 但任何 编辑器都可以构建扩展 在代码编辑器中 我将创建 一个文件夹存放所有文件 打好基础 每个扩展 首先需要的是一个清单文件 清单是一个JSON格式的文件 用于告知浏览器扩展的信息 以及它能做什么 可以把清单想象成 你扩展的身份证 它包含各种信息 例如扩展的名称 描述和版本号 接下来 我将添加一个 存放扩展图标的图片文件夹 图标可以出现 在各种不同的地方 比如工具栏... 或扩展设置 根据显示位置的不同 需要不同尺寸的图标 所以 我将以SVG格式添加图标 Safari会完美处理图标缩放 让我专注于重要的事情 比如打开代码编辑器 展示实际效果 在清单文件中 我已添加了扩展的图标 图标位于 我的图片文件夹中 为了查看效果 我保存更改 然后前往Safari 来加载扩展 在Safari中加载扩展 非常简单 只需使用Command+逗号 打开Safari设置 点击高级设置面板 并勾选 "显示适用于网页开发者的功能" 这将启用开发者面板 然后我就可以 添加一个临时扩展 由于这个扩展尚未经过 代码签名验证 我需要允许未签名扩展 允许后 我选择包含 扩展资源的文件夹 就这样 我的扩展 已加载到Safari中了! 大多数扩展都会有 某种供用户交互的UI 对于我的扩展 我希望 让用户能够添加 分散注意力的网站到屏蔽列表 所以我需要添加一些自定义UI 有几种方式可以实现 一种方式是使用 扩展的操作按钮 这是在Safari工具栏中 为扩展添加的按钮 点击后 Safari会显示弹出窗口 展示你为其定义的UI 弹出窗口的文件名 或清单中定义的任何资源 可以是任何你想要的名称 只要与正确的清单键关联 让Safari知道它是什么 以及如何使用它 在这个示例中 加载 默认弹出窗口的文件设置为"popup.html" 对于我的扩展 在弹出窗口中显示屏蔽列表 可能会让UI看起来有些拥挤 所以我将使用 扩展的选项页面来显示 这将是一个完整页面 用户可以在此设置扩展选项 现在 我将回到代码编辑器 添加这个更改 在清单中 我已定义 了选项页面 我将在扩展文件夹中 为它添加一个文件 在添加此页面的完整UI之前 我先从简单的内容开始 比如Hello World 在Safari中 我可以通过 重新加载扩展来测试更改 太棒了! 我的新更改生效了 扩展 现在有了这个设置按钮 点击该按钮会打开 我扩展的选项页面! 设置好之后 我的扩展还需要做更多 而不仅仅是显示"Hello World" 才能真正发挥作用 所以我设计了一个页面 允许用户在 轻量模式和完全模式之间切换 我已经使用HTML CSS和JavaScript 编写了这个界面 看起来很漂亮也很互动 但我需要将它连接起来 现在我将了解如何升级扩展 以开始屏蔽内容 我将使用声明式网络请求API 它将赋予我的扩展 屏蔽 修改或重定向 网络请求的能力 这个API驱动着 我最喜欢的那类网页扩展 有了这种能力 扩展可以过滤 广告等网页内容 以及在用户浏览网页时 针对他们的追踪器 但为了让我的扩展 访问这些功能 我需要为它添加一个权限 权限是扩展告知Safari 所需访问权的方式 比如访问Cookie 将数据保存到存储 或写入剪贴板 对于内容屏蔽 我需要添加 声明式网络请求权限 我可以在扩展的 清单中完成这一步 设置好之后 我就可以开始定义规则了 一条规则具有ID 优先级 以及满足条件时应触发的操作类型 当条件满足时 例如这条规则会屏蔽 所有前往webkit.org的导航 有两种方式可以定义规则 一种方式是在 清单中定义它们 这些称为静态规则 当你已经知道要使用 哪些规则时非常适用 但如果需要一些灵活性 你可以在运行时动态添加规则 使用JavaScript 我将选择动态方式 因为我不知道要屏蔽哪些网站 直到用户将它们添加到列表中 我将把这个逻辑放在 utilities文件夹中的rules.js文件里 然后我将使用host.js文件 来创建规则 当用户将网站添加到屏蔽列表时 我将回到代码中 连接这些逻辑 在扩展的清单中 我已添加 了声明式网络请求权限 我还将之前的选项页面替换为 HTML CSS和JavaScript文件 这些是我已经为扩展创建的文件 我还添加了包含 两个新文件的utilities文件夹 要添加规则 我前往rules.js文件 由于这些规则需要指定ID 我添加了一个辅助方法 将网站的主机 映射到唯一的整数ID 现在我创建一条规则 指定ID 类型为"block" 以及urlFilter 来匹配网站的主机 然后使用声明式网络请求的 updateDynamicRules API 将规则添加到我的扩展 在我的host文件中 当网站被添加到列表时 如果扩展处于 完全屏蔽模式 就可以添加规则 回到Safari 我重新加载以更新扩展

    在选项页面 我将webkit.org添加到列表

    当我访问该网站时 它被屏蔽了! 我的扩展现在可以屏蔽 对列表中网站的导航了 但我不太喜欢 那个弹出的错误页面 我更希望将用户引导到 更有意义的地方 比如自定义页面 一个我为扩展专门设计的页面 这就是重定向规则 发挥作用的地方 这条规则与之前的屏蔽规则类似 但类型是重定向 我可以指定extensionPath 作为用户将要进入的页面 但在进行这个更改之前 我需要为扩展添加主机权限 要屏蔽网络请求 扩展不需要访问页面 但对于重定向网络请求 扩展确实需要访问权限 所以在扩展的清单中 我将使用 declarativeNetRequestWithHostAccess 权限代替 由于我的扩展不需要 预先请求任何网站的访问权限 我可以使用可选主机权限 并在运行时请求网站访问权 主机权限告知Safari 你的扩展想访问哪些网站 你可以将它们设置为 一组匹配模式 每个模式由 协议 主机和路径组成 由于任何网站都可以被添加到列表 我将使用一个 可以匹配所有URL的模式 如果你的扩展需要 明确访问某个网站才能运行 可以改用主机权限 但扩展不会自动 获得对该网站的访问权 我们为扩展设计了权限模型 以尊重用户隐私 由于用户的浏览体验 可能会暴露个人数据 我们让用户掌控 由他们决定 扩展可以访问哪些网站 如果你明确请求访问权限 Safari会在扩展的 操作按钮上显示徽章 点击该按钮会弹出提醒 询问用户 是否要授予扩展 访问该页面的权限 如果用户选择允许访问 图标将变为彩色 通知他们扩展在 该页面处于活跃状态 我的扩展不需要 预先访问任何网站 这就是我选择 可选主机权限的原因 这样 我的扩展需要时 就可以请求任何网站的访问权 在清单中 我已将权限更改为 declarativeNetRequestWithHostAccess 我的扩展现在可以在运行时 请求访问任何网站了 现在在规则文件中 我将创建重定向规则 它与之前的屏蔽规则非常相似 但现在类型是重定向 并且包含指向 我自定义扩展页面的路径 我还在扩展文件夹中 添加了该页面的资源 现在不再屏蔽导航 而是将用户重定向到一个页面 一个我为扩展设计的页面 由于扩展需要访问该网站 我将请求访问权限 使用permissions.request API 请求访问该域名及其子域名 为了查看这种体验 我将在Safari中更新扩展

    在选项页面中 我将添加webkit.org

    现在 在网站被添加之前 我收到提示要授予扩展访问权限 很好! 这正是我所期待的 我允许访问并刷新页面

    太棒了! 导航被重定向到 我的自定义扩展页面 我的扩展进展得真好! 它现在可以将前往 干扰性网站的导航重定向了 但说实话 与一些你最喜欢的网站 完全断联可能很难 所以我将添加一个 允许浏览最多10分钟的模式 并在页面上 显示倒计时 为了实现这一点 我需要一种 直接将内容注入页面的方式 这就是内容脚本 发挥作用的地方 内容脚本赋予扩展 读取和修改的能力 网页内容 脚本可以是静态的 直接在清单中声明 包含它们将运行的网站 的文件和匹配模式 如果你已经知道 要针对哪些网站 这很有效 但在我的情况下 直到网站 被添加到列表我才知道 所以我将动态添加它们 使用注册内容脚本API 这些与静态脚本完全相同 但有两个额外字段 一个ID和一个持久化标志 将此设置为true意味着 脚本将保留 在Safari重新启动之后 要使用此API 我将在 清单中添加scripting权限 以及在utilities文件夹中 添加新的scripting.js文件 在这里 我将定义内容脚本 我将给它一个ID 计时器的JavaScript文件 样式的CSS 以及覆盖该网站域名 和所有子域名的匹配模式 然后我将此标志设置为true 接着 我将使用 注册内容脚本API添加脚本 回到我的addHost方法 现在 当用户将网站添加到屏蔽列表时 我将添加一个内容脚本 来为该网站显示计时器 当扩展处于完全屏蔽模式时 重定向会在页面加载前触发 所以我可以 始终注册脚本 现在选择轻量屏蔽模式 我将webkit.org添加到列表 当我访问该网站时 页面上会显示10分钟的计时器

    我的扩展离我想发布 到世界的版本已经非常接近了 但有一件事 我想先修复 正如你可能注意到的 每次我重新加载扩展 我都必须重新将 同一个网站添加到列表 这是因为我将所有信息 存储在内存中 当扩展重新加载时 该状态就消失了

    我可以使用storage API 来保留这些数据 Safari支持两种 存储区域 Session Storage适合 快速的内存操作 不需要在重启后保留的数据 但我希望我的屏蔽列表 持久保存 所以Local Storage (将数据写入磁盘)是正确选择

    要使用storage API 我将在清单中添加权限 然后 我将在 utilities文件夹中添加一个新文件 在此文件中 我将定义几个辅助方法 用于更新和获取存储中的主机 以及几个用于保存和 获取屏蔽模式的方法

    回到我的addHost方法 现在 当用户添加新网站时 我可以更新 存储中的主机列表 我还可以使用 存储的列表来显示屏蔽列表 有了这个更改 屏蔽列表将始终显示 所有已添加网站的完整列表! 同样地 当用户在 两种模式之间切换时 我将把更改保存到存储 如果扩展处于 完全屏蔽模式 我可以为所有网站 创建重定向规则 为了看到storage API的效果 在Safari中 我将屏蔽模式改为"Full" 并将一个网站添加到列表

    现在 当我重新加载扩展时 我刚才做的更改 仍然保留! 很好! 使用storage API解决了扩展的 一个持久化问题 但还有另一个问题需要解决 已注册的内容脚本 在Safari重启后仍然保留 但在扩展更新后则不会 所以如果用户更新了我的扩展 他们将失去内容脚本 为了解决这个问题 我需要 让扩展知道它已被更新 这样它就可以从存储中 读取主机并重新创建脚本 最佳位置是在 后台页面或Service Worker中 两者都可以做相同的事情 比如管理扩展的生命周期 监听浏览器事件并在 扩展的各个部分之间传递消息 Safari同时支持这两种 所以完全取决于你的偏好! 我喜欢后台页面 因为它们 可以访问DOM 所以我选择它 要添加后台页面 我将在清单中指定它 然后 我将文件 添加到扩展文件夹中 在这里 我将注册 onInstalled事件 这将让我的扩展知道 它已更新到新版本 当这种情况发生时 我将从存储中读取主机 并重新注册内容脚本 让扩展能够从存储中读写数据 带来了巨大的改进 我认为是时候看看如何 将扩展发布到App Store了 一种方式是使用 App Store Connect App Store Connect是你可以 上传 提交和管理扩展的地方 无论你是刚构建好自己的扩展 还是想将 现有扩展迁移到Safari 最棒的部分呢? 你可以在任何浏览器中 完成这些操作 无需使用Mac 首先 我前往 developer.apple.com并注册 Apple Developer Program 注册后 我前往 appstoreconnect.apple.com 由于Safari网页扩展 需要封装在一个包含App中 我可以使用App Store Connect 为我创建这个App 创建App时 我需要指定一些内容 例如我想让扩展 在哪些平台上可用 我将选择iOS和macOS 这将使我的扩展 在iPhone iPad Mac上可用 以及作为Apple Vision Pro 上的兼容App 我还将设置Bundle Identifier 这是我App的唯一ID 添加所有详细信息后 我将 切换到Xcode Cloud标签 向下滚动到 Safari网页扩展打包程序 上传我扩展的资源 上传完成后 Safari网页扩展打包程序 将在几分钟内完成 打包我的扩展! 完成后 我可以查看任何问题 或采取后续步骤测试扩展 使用TestFlight TestFlight允许我 分发Beta版本 这样我可以持续改进 并收集用户反馈 然后再将扩展 提交到App Store 一旦我准备好提交 我将前往发布标签 添加最后的细节 比如扩展运行中的截图 以及帮助用户了解的描述 我扩展的功能和特性 添加所有详细信息后 我将 选择构建版本并提交审核 这就是如何使用 App Store Connect分发扩展的方法! 在本节课中 我展示了 标准WebExtension API和功能如何 共同构建 可定制的浏览体验 但如果我告诉你 你的扩展 可以超越网页平台呢 我将引导你了解如何使用 原生消息传递来访问功能 这些平台提供的功能 从这里 我需要使用Xcode 最简单的方式是使用 Safari网页扩展打包程序工具 在Terminal中运行此命令将 为我创建并启动一个Xcode项目 它将包含我的App和网页扩展 然后 我可以将它们连接起来 发送和接收消息 我们称之为原生消息传递 把它想象成 三个人传递便条 我扩展中的JavaScript 开始发起 中间的App Extension 接收该消息 并将其传递给原生App 然后App执行操作 并以相同方式将结果发送回来 对于我的App 我将让它 帮助扩展保护设置 在对屏蔽列表进行更改前 要求生物识别认证 首先 我将添加 nativeMessaging权限 在我扩展的清单中 然后 在扩展的后台页面中 我将添加一个方法来发送消息 从我的扩展发送到其原生App 我收到的消息将告知我 认证是否成功 为了让我的App 接收消息 我需要修改 SafariWebExtensionHandler 一个包含在打包程序工具 为我创建的文件中的类 棒的是 它附带一个模板 已经允许我的App 从我的扩展接收消息 我需要做的只是几处调整 比如解析消息中的 requestBioAuth键 然后 我将使用系统API 请求用户生物识别认证 一旦用户完成认证 App将消息发送回来 将结果传递给网页扩展 我将进入Xcode构建扩展 并在Safari中测试更改 在Xcode中 我已完成向扩展 发送和接收消息的更改 在Project Navigator中 我的项目包含了 网页扩展的所有资源 我将使用Command+B快捷键 构建扩展 在Safari中 我启用扩展 打开选项页面 并将webkit.org添加到列表

    在添加之前 我收到提示 通过触控ID认证 认证后 网站被添加到列表中 这就是如何使用原生消息传递 让App 和网页扩展协同工作 有了这些 我可以自豪地说 我的扩展已准备好分发了 我将使用Xcode来完成

    我将先构建一个归档文件 由于我之前使用 App Store Connect创建了一个构建版本 我需要确保这个构建版本号 比上一个高一步 在Organizer窗口中 我将分发扩展 这就是如何从零开始 创建和分发Safari网页扩展的方法 从零开始 今天我们涵盖了很多内容 无论你是刚刚起步 还是想进一步拓展扩展功能 我希望你已经准备好 将想法转化为现实 我迫不及待地想看到你构建的作品

    如果你还没有的话 下载示例代码项目 来玩玩今天介绍的 一些API 你可以通过查看跨浏览器 文档来进一步了解 MDN上关于网页扩展的文档 最后 通过Feedback Assistant 提供反馈 或在bugs.webkit.org上提交Bug 当你在Safari 27上 测试网页扩展时 感谢你与我一起踏上这段旅程 WWDC快乐!

    • 3:44 - Manifest file

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0
      }
    • 4:29 - Adding an extension icon

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "icons": {
              "512": "images/icon.svg"
          }
      }
    • 5:30 - Adding an action button

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
      
          "action": {
              "default_popup": "popup.html"
          }
      }
    • 6:17 - Adding custom UI to your extension

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "options_ui": {
              "page": "options.html"
          }
      }
    • 6:30 - Including the UI in the extension manifest

      {
          "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"
          }
      }
    • 6:40 - Hello World

      <!DOCTYPE html>
      <html>
          <body>
          <p>Hello World</p>
          </body>
      </html>
    • 8:18 - Adding declarativeNetRequest permission

      {
          "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"
          },
        
          "permissions": [ "declarativeNetRequest" ]
      }
    • 8:22 - Blocking network requests

      // block rule
      {
          id: 1,
          priority: 1,
          action: {
              type: "block"
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 8:41 - Modifying network requests

      {
          "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"
          },
        
          "permissions": [ "declarativeNetRequest" ],
      
          "declarativeNetRequest": {
              "rule_resources": [
                  {
                      "id": "ruleset_id",
                      "enabled": true,
                      "path": "rules.json"
                  }
              ]
          }
      }
    • 8:50 - Updating dynamic rules

      await browser.declarativeNetRequest.updateDynamicRules({
          addRules: [ rule ]
      })
    • 9:19 - Wiring up the static declarativeNetRequest rules

      {
          "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"
          },
        
          "permissions": [ 
            "declarativeNetRequest" 
          ]
      }
    • 9:40 - Adding block rules dynamically

      // A helper function to map the host to the declarative net request rule 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")
      	}
      }
    • 10:10 - Handling adding hosts to the settings

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      export async function addHost(host, blockingMode) {
        if (!host)
          return
        
        if (blockingMode === "full")
          await createRules([host])
      }
    • 10:48 - Redirecting network requests

      {
          id: 1,
          priority: 1,
          action: {
              type: "redirect",
              redirect: {
                  extensionPath: "/blocked.html"
              }
          },
          condition: {
              urlFilter: "||webkit.org",
              resourceTypes: [ "main_frame" ]
          }
      }
    • 11:17 - Declaring optional host permissions

      {
          "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"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "https://webkit.org/*" ]
      
      }
    • 11:54 - Declaring optional host permissions for all sites

      {
          "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"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ]
      
      }
    • 13:12 - Add the redirect rule

      // A helper function to map the host to the declarative net request rule 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"]
      		}
      	}
      }
      
      function createRedirectRule(host) {
      	return {
      		id: hostToRuleID(host),
      		priority: 1,
      		action: {
      			type: "redirect",
      			redirect: { extensionPath: "/blocked.html" }
      		},
      		condition: {
      			urlFilter: `||${host}`,
      			resourceTypes: ["main_frame"]
      		}
      	}
      }
      
      export async function createRules(hosts) {
      	try {
      		await browser.declarativeNetRequest.updateDynamicRules({
      			addRules: hosts.map(createRedirectRule)
      		})
      	} catch {
      		console.log("Failed to create declarative net request rules")
      	}
      }
    • 13:42 - Dynamically ask for host permissions

      import { createRules, removeAllRules, removeRule } from './rules.js'
      
      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])
      }
    • 14:55 - Defining content scripts

      {
          "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"
          },
        
          "permissions": [ "declarativeNetRequestWithHostAccess" ],
          "optional_host_permissions": [ "*://*/*" ],
        
          "content_scripts": [
              {
                  "js": [ "content.js" ],
                  "css": [ "content.css" ],
                  "matches": [ "*://*.webkit.org/*" ]
              }
          ]
      }
    • 15:13 - Dynamically registering content scripts

      let script = {
          id: "id",
          js: [ "content.js" ],
          css: [ "content.css" ],
          matches: [ "*://*.webkit.org/*" ],
          persistAcrossSessions: true
      }
      
      await browser.scripting.registerContentScripts([ script ])
    • 15:31 - Adding the scripting permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 15:41 - Registering content scripts

      // scripting.js
      
      function contentScript(host) {
          return {
              id: `cs-${host}`,
              js: [ "content.js" ],
              css: [ "content.css" ],
              matches: [ `*://${host}/*`, `*://*.${host}/*` ],
              persistAcrossSessions: true
          }
      }
      
      export function registerScripts(hosts) {
          const scripts = hosts.map(contentScript)
          try {
              await browser.scripting.registerContentScripts(scripts)
          } catch {
              console.log("Failed to register content scripts")
          }
      }
    • 16:02 - Adding a host

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      }
    • 17:06 - Web extensions storage APIs

      await browser.session.storage.set({
        key: value
      })
      
      await browser.local.storage.set({
        key: value
      })
    • 17:21 - Adding storage permission to the web extension 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_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ]
      }
    • 17:30 - Saving data with storage

      // storage.js
      
      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
      }
    • 17:41 - Persisting hosts to storage

      // host.js
      
      export async function addHost(host, blockMode) {
          if (!host)
              return
      
          const granted = await browser.permissions.request({
              origins: [`*://${host}/*`, `*://*.${host}/*`]
          })
      
          if (!granted)
              return
      
          if (blockingMode === "full")
              await createRules([ host ])
      
          await registerScripts([ host ])
      
          let existingHosts = await getHosts()
          let updatedHosts = [ ...existingHosts, host ]
          await updateHosts(updatedHosts)
      }
    • 17:51 - Reading from storage

      // options.js
      
      let existingHosts = await getHosts()
      let blockMode = await getBlockMode()
      
      displayBlocklist(existingHosts)
    • 18:00 - Switching block modes

      // host.js
      
      export async function userDidSwitchMode(blockMode) {
          await saveBlockMode(blockMode)
      
          if (blockMode === "full") {
              let hosts = await getHosts()
              await createRules(hosts)
          } else
              await removeAllRules()
      }
    • 19:01 - Adding a background script

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 19:39 - Background script

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
    • 22:49 - Package your web extension into an app for Xcode

      xcrun safari-web-extension-packager --copy-resources /path/to/ShinyOnTrack
    • 23:32 - Adding the nativeMessaging permission

      {
          "manifest_version": 3,
          "name": "Shiny OnTrack",
          "description": "Stay on track while you browse the web",
          "version": 1.0,
        
          "icons": {
              "512": "images/icon.svg"
          },
        
          "options_page": "options.html",
        
          "permissions": [
              "declarativeNetRequestWithHostAccess",
              "scripting",
              "storage",
              "nativeMessaging"
          ],
        
          "optional_host_permissions": [ "*://*/*" ],
        
          "background": {
              "scripts": [ "background.js" ],
              "type": "module"
          }
      }
    • 23:40 - Sending a native message

      // background.js
      
      import { registerScripts } from "./utilities/scripting.js"
      import { getHosts } from "./utilities/storage.js"
      
      browser.runtime.onInstalled.addListener(async (details) => {
          if (details.reason !== "update")
              return
      
          const hosts = await getHosts()
          await registerScripts(hosts)
      })
      
      export async function requestBioAuth() {
          const message = { message: "requestBioAuth" }
          const response = await browser.runtime.sendNativeMessage(message)
          return response?.success
      }
    • 23:55 - Handling native messages

      // SafariWebExtensionHandler.swift
      
      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)
                      }
                  }
              }
          }
      }
    • 24:25 - Replying to a native message

      // SafariWebExtensionHandler.swift
      
      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)
          }
      }
    • 0:00 - Introduction
    • Learn how Safari web extensions — built with HTML, CSS, and JavaScript and packaged inside an app — can run across iOS, iPadOS, macOS, and visionOS. Preview the distraction-blocker extension built throughout the session, which offers a 10-minute light mode and a full redirect mode.

    • 3:23 - Get started
    • Set up an extension from scratch by writing a manifest.json file, then add a popup UI so the extension is reachable from Safari's toolbar. The same project runs unchanged across every Apple platform that ships Safari.

    • 7:23 - Block content
    • Use the declarativeNetRequest API to block, modify, and redirect network requests, and declare the host permissions — including optional host permissions — that let users grant access on the sites where the extension should run.

    • 14:40 - Modify webpages
    • Inject content into pages with content scripts to render a countdown timer on distracting sites. Register scripts dynamically with the scripting API and persist user preferences and per-host state using the storage API and a background service worker.

    • 19:53 - Package and distribute
    • Submit a Safari web extension to the App Store using App Store Connect, and share beta builds with testers through TestFlight.

    • 22:33 - Communicate with your app
    • Generate an Xcode project with the Safari Web Extension Packager, then use native messaging to pass requests between the JavaScript extension and its containing app — unlocking platform features like Local Authentication that aren't available to web APIs.

    • 26:04 - Next steps
    • Download the sample project, explore the cross-browser WebExtensions documentation on MDN, and file feedback through Feedback Assistant or bugs.webkit.org.

Developer Footer

  • 视频
  • WWDC26
  • 创建 Safari 浏览器的网页扩展
  • 打开菜单 关闭菜单
    • iOS
    • iPadOS
    • macOS
    • Apple tvOS
    • visionOS
    • watchOS
    打开菜单 关闭菜单
    • Swift
    • SwiftUI
    • Swift Playground
    • TestFlight
    • Xcode
    • Xcode Cloud
    • SF Symbols
    打开菜单 关闭菜单
    • 辅助功能
    • 配件
    • Apple 智能
    • App 扩展
    • App Store
    • 音频与视频 (英文)
    • 增强现实
    • 设计
    • 分发
    • 教育
    • 字体 (英文)
    • 游戏
    • 健康与健身
    • App 内购买项目
    • 本地化
    • 地图与位置
    • 机器学习与 AI
    • 开源资源 (英文)
    • 安全性
    • Safari 浏览器与网页 (英文)
    打开菜单 关闭菜单
    • 完整文档 (英文)
    • 部分主题文档 (简体中文)
    • 教程
    • 下载
    • 论坛 (英文)
    • 视频
    打开菜单 关闭菜单
    • 支持文档
    • 联系我们
    • 错误报告
    • 系统状态 (英文)
    打开菜单 关闭菜单
    • Apple 开发者
    • App Store Connect
    • 证书、标识符和描述文件 (英文)
    • 反馈助理
    打开菜单 关闭菜单
    • Apple Developer Program
    • Apple Developer Enterprise Program
    • App Store Small Business Program
    • MFi Program (英文)
    • Mini Apps Partner Program
    • News Partner Program (英文)
    • Video Partner Program (英文)
    • 安全赏金计划 (英文)
    • Security Research Device Program (英文)
    打开菜单 关闭菜单
    • 与 Apple 会面交流
    • Apple Developer Center
    • App Store 大奖 (英文)
    • Apple 设计大奖
    • Apple Developer Academies (英文)
    • WWDC
    阅读最近新闻。
    获取 Apple Developer App。
    版权所有 © 2026 Apple Inc. 保留所有权利。
    使用条款 隐私政策 协议和准则