Safari 浏览器插件(扩展)
前期准备
- http://developer.apple.com 开发者账号注册 需要 Apple ID
- Safari Extension 旧版文档 适用于 2018 年前, Safari 12版本之前,大部分接口不可再使用
- Safari App Extension 新版文档 旧版中关于与浏览器APP 相关的接口,改为通过 Swift 实现,适用于 Safari 12版本及后续版本
- 有关插入脚本或者样式的文档
- 有关 NSExtension: 控制访问权限的配置
- 上下文菜单与工具栏开发的文档
- safari 扩展的开发中需要用到一部分 Swift 语言,学会这篇文章Swift 学习记录中的内容就够用了
1. 创建项目及基本开发步骤
1. 创建 Safari 扩展应用
进入 Xcode 选择创建新项目,然后依次选择 macOS -> Safari Extension App
2. 创建 应用名称 及 上传应用 logo 不同像素都需要对应的图片
3. 更新配置文件
- 大部分的开大及配置都在 [ExtensionName] Extension 文件夹下,也就是具有 script.js 的文件夹
- 这个文件夹中的 info.plist 就是对应 chrome 插件 manifest.json 的功能的一个配置文件但是是一个 XML 的文件格式 Xcode 会将其转换成一个UI界面来方便操作
- 其中 NSExtension 这个key 用来控制所有的访问权限,例如:什么站点可以插入脚本|样式;需不需要上下文菜单;需不需要工具栏功能等。
- 如图中的 SFSafariContentScript 用来配置(Array),需要插入到网站中的脚本,与 chrome 中 的 contentScript 对应,可以访问网站的 window 对象,可以操作其中的dom,网站中如果有 iframe 嵌套,那么每个 iframe 也都会被注入脚本。
- SFSafariToolbarItem 用来配置显示在Safari 工具栏中的控件
- SFSafariContextMenu 用来配置显示 上下文菜单
- SFSafariWebsiteAccess 用来配置 可以接入脚本的网站 默认是 Allowed Domains: *.webkit.org,如果要插入脚本到所有站点,则可以 Allowed Domains 删除,将 Level: All
4. 扩展打包及调试
注意: 每次打包之前记得清空上次打包,否则浏览器中无法使用扩展
打包:
- 更改代码 cmd + c 保存
- shift + cmd + k 删除旧的 bundle
- cmd + b 重新打包
- Safari 中浏览变更
调试:
- cmd + , 进入 Safari 浏览器的偏好设置,在高级中勾选 “在菜单栏中显示开发菜单”
- 打开开发菜单,勾选最下边的 “允许未签名的扩展”
- cmd + , 再次进入 Safari 浏览器的偏好设置,点开扩展选项,可以在这里找到,我们刚刚打包后的扩展应用,将其选中即可在Safari 浏览器中使用
- 打开 Safari 浏览器在如上SFSafariWebsiteAccess配置中对应的网站,打开网页检查器,在“来源”选项卡下即可看到,我们插入的扩展脚本
- 插入脚本的日志在控制台就可查看,但是扩展的日志要麻烦一点,扩展的日志 NSLog 需要我们打开 Mac 自带的应用 “Console app” / “控制台” 然后筛选出进程名称为我们浏览器扩展名称的的日志,如图
5. 版本发布
- 前提是要有开发者的账号
- 选择 Product -> Archive
- 然后在弹窗中选中 Distribute App
- 可以选择 发布到 App Store Connect 也可以选择 Copy App 打包到本地,打包到本地的应用可以传给其他人测试用
2. 开发说明
1. 设置浏览器访问权限
在 Info.plist 文件中的 NSExtension > SFSafariWebsiteAccess 中指定可访问的网址和域,只有这里指定的站点可以在 扩展中使用 SFSafariPageProperties 对象
SFSafariWebsiteAccess 有两条配置 Level | Allowed Domains
- Level: All 所有网站都可访问 | None 不可通过注入脚本来访问 | Some 只有 Allowed Domains 中的域可以访问,这个时候如果不设置 Allowed Domains 则所有域都不可访问
- Allowed Domains: 允许访问的域的数组,配合 Level: Some 使用
2. 注入脚本与扩展之间的通信
关于 注入脚本 与页面内容间的通信请看 关于浏览器插件开发过程中注入脚本与页面内容间的通信
- 插入的脚本及样式直接在 NSExtension 中配置的文件地址指定的文件中开发即可,
- 脚本与浏览器扩展交互主要靠 postMessage 来实现
- 浏览器在 SFSafariExtensionHandler.swift 文件中处理接收的事件
- 脚本中可以正常使用 window document 对象,除此之外支持 safari 对象,它是 SafariAppExtensionNamespace 类的实例,提供了关于应用扩展的一些信息,并且支持注入脚本与扩展之间的通信
safari.extension
是应用扩展的代理,用来检索扩展的信息,和向扩展发出消息safari.self
注入脚本的代理,用来响应来自扩展的消息
// 脚本中发出消息
safari.extension.dispatchMessage("CHANGE_SOMETHING", {key: 'value'}); // 第二个参数选填,但必须是 对象
// 脚本中接收消息
safari.self.addEventListener("message", function (event) {
console.log(event.name); // 'CHANGE_SOMTHING_SUCCESS'
console.log(event.message); // {myKey: myValue}
});
- 扩展处理插入脚本间的通信是在 SafariExtensionHandler.swift 文件中,其中 messageReceived 用来接收信息,page.page.dispatchMessageToScript 用来向插入的脚本发出信息
// extension 中接收消息
override func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?) {
// extension 中发出消息
page.dispatchMessageToScript(withName: "CHANGE_SOMTHING_SUCCESS", userInfo: ["myKey", "myValue"])
}
- 注入脚本中可以访问扩展包中的资源:在注入脚本中通过
safari.extension.baseURI
可以拿到一个关于当前扩展的资源地址,这个地址下的文件可以被脚本当做 URL 引用,例如:safari.extension.baseURI + 'ToolbarItemIcon.pdf'
可以拿到默认的工具栏图标
3. SFSafariExtensionHandler
一个 用于 Safari 扩展的事件处理类,其包含的详细方法可以查看 SFSafariExtensionHandling : SFSafariExtensionHandler 实现的协议,工具栏、上下文菜单、插入脚本等的事件处理都在这里,我们这里用到的就是 messageReceived 方法 。
4. SFSafariPage
可以通过 SFSafariExtensionHandler
func messageReceived(withName messageName: String, from page: SFSafariPage, userInfo: [String : Any]?)
方法的第二个参数 page 获得,page 表示当前接收信息的来源页面,page 就是 SFSafariPage 这个类的实现,SFSafariPage 具有以下方法
dispatchMessageToScript(withName:userInfo:)
从 extension 向注入的脚本发送消息,withName: 消息名称,userInfo:传入的数据getPropertiesWithCompletionHandler
从page 页面获得网页的属性,网页的属性包括:isActive: 是否处于活动状态;title: 网页标题;url: 页面的 URL;usesPrivateBrowsing:是否使用了无痕窗口reload
触发 Safari 重载页面
5. SFSafariApplication
一个浏览器应用的代理,在 SFSafariExtensionHandler.swift 文件中可以直接访问,通过它可以访问活动的 Safari 窗口、打开新的窗口、更新窗口工具栏等,这个类是没有实例的, SFSafariApplication 文档
getActiveWindow
回调的参数是一个 SFSafariWindow 对象,作用是获得活动的浏览器窗口
// 获得活动的窗口,然后打开新的tab 页面
SFSafariApplication.getActiveWindow(completionHandler: { activeWindow in
activeWindow?.openTab(with: URL(string: "https://www.baidu.com")!, makeActiveIfPossible: true, completionHandler: { _ in NSLog("OPEN SUCCESS")})
})
openWindow(with:completionHandler:)
用来打开一个新的浏览器窗口
6. SFSafariWindow
代理浏览器的窗口,可以用来打开 Tab 或者获取活动的Tab
getActiveTab(completionHandler: @escaping (SFSafariTab?) -> Void)
获取活动的选项卡,回调的参数是 SFSafariTabopenTab(with url: URL, makeActiveIfPossible activateTab: Bool, completionHandler: ((SFSafariTab?) -> Void)? = nil)
打开一个新的选项卡,第二个参数用来设定打开时激活这个选项卡,回调的参数是 SFSafariTab
7. SFSafariTab
代理浏览器的选项卡,可以用来获得选项卡下的页面对象
getActivePage(completionHandler: @escaping (SFSafariPage?) -> Void)
获取当前选项卡下活动的页面,回调的返回值是 SFSafariPage 可以通过他再与新打开页面的插入的脚本进行通信getPagesWithCompletionHandler(_ completionHandler: @escaping ([SFSafariPage]?) -> Void)
获得此选项卡下活动的页面及其他可能后台运行的页面
8. 如何在 extension 中存取数据
- 找到
SafariExtensionViewController.swift
这个文件,声明一个类的属extensionState
作为数据的 state - 然后在
SafariExtensionHandler.swift
中就可以通过SafariExtensionViewController.shared.extensionState
来访问这个属性了,在浏览器打开的阶段这个属性会一直存在,可以通过他来作为临时存储 - 为什么不直接在
SafariExtensionHandler.swift
中做数据存储?: 经测试SafariExtensionHandler.swift
中的变量在打开新页面都是新的,不能作为页面间的数据存储