用 Web 技術爲 Safari 編寫擴展

Python實戰社羣

Java實戰社羣

長按識別下方二維碼,按需求添加

掃碼關注添加客服

進Python社羣▲

掃碼關注添加客服

進Java社羣

作者:希德,iOS 開發者,前“有經驗的前端開發工程師”,就職於網易嚴選。正在寫書《Thinkable SwiftUI》(嚴重拖稿中)

Session 10665: https://developer.apple.com/videos/play/wwdc2020/10665

今年(2020)蘋果宣佈引入一種新的 Safari 擴展類型,這種類型使用 Web 技術來爲 macOS 上的 Safari 增強功能。在進入正題之前,讓我們先回顧下目前 Safari 業已存在的擴展生態系統。目前包含以下類型的擴展;

  • 內容攔截擴展(支持 iOS、macOS)

  • 分享擴展(支持 iOS、macOS)

  • Safari App 擴展(只支持macOS)

現在的 Safari 插件開發對於熟悉 Objective-c 或者 Swift 的開發者來說非常容易入門上手,但事實上,熟悉 JavaScript、HTML 和 CSS 的 web 開發者要比熟悉 Objective-C 或者 Swift 的開發者多的多;而且除了 Safari 插件外,其他主流的瀏覽器的插件技術都是基於 HTML 等 web 技術來構建(事實上,Safari 擴展在歷史上也是可以用 Web 技術來實現的)。

近年來,Apple 在思考如何把更多的 iOS App 的生態引入到 macOS 的生態,所以他們引入了 Mac Catalyst 技術作爲橋接;同樣的,如果需要把其他瀏覽器的插件生態導入到 Safari 的生態,不得不重新啓用 web 技術來支持這個目標;所以 Safari Web 插件採用了業界瀏覽器插件開發的事實標準[1]。使用和其他瀏覽器插件一樣的接口,有助於消除開發者的學習成本。同時,蘋果的插件技術選型,讓從其他瀏覽器的插件轉化爲 Safari Web 插件成爲順理成章的事情 ——把 Safari App Extension升級爲Safari Web Extension不是基於技術優劣的選擇而是基於市場的考慮。Safari Web Extension 開發插件技術棧類似其他瀏覽器上的插件開發,同時也符合蘋果一直在強調的隱私控制規範。

下面將介紹的內容如下;

  1. 如何創建 Safari Web 擴展

  2. 重視隱私和權限控制

  3. 如何調試插件

  4. 和 App 通訊的方式

本文不涉及如何移植其他瀏覽器插件

在開始編寫第一個 Safari Web Extension  之前,我們需要了解下,Safari Web Extension 是如何打包、安裝到 Safari 裏的。Safari Web Extension 必須包含在 Native App 內。當用戶從 App Store 下載到電腦後,插件會被自動安裝到 Safari 裏。

如何創建 Safari Web 擴展

在編寫插件之前,你需要 Xcode 12,但Safari Web Extension 是運行在 Safari 14 上的,所以你還需要升級你的 macOS 到 11.0 macOS Big Sur(macOS 10.15 據說也可以,沒有嘗試)。創建 Safari Web Extension  有兩種途徑,一種是爲原有的 App 創建一個擴展(Safari Extension);或創建 Safari Extension App  ,同時創建一個宿主 App 和 擴展。這裏我們採用第二種方式。

依次選擇菜單 File ->  NewAnd Target -> Safari Extension App,

創建  Safari Extension App

Xcode 會自動創建好模板的目錄結構,

從目錄結構上看,一種分爲 3 部分。

圖例2
  • 宿主 App,當宿主 App 啓動時可以執行一些 macOS 特有能力的操作,如在 App 界面內做檢索、分享等操作。主要文件,ViewController.swift

  • Native Extension 部分。它有部分 macOS 平臺的接口的執行權限,和 宿主 App 擁有各自獨立的沙盒。最大的區別是它不能有自己的界面。主要文件,SafariWebExtensionHandler.swift

  • Extension Web 部分。它是和網頁打交道的主戰場,包括展示自定義界面、修改當前活動網頁內容,並保存部分信息到另外兩部分等操作。主要文件,_locales\icons\manifest.json\background.js\content.js\popup 等。

其中最最重要的是 Extension Web 部分,我們打開 MDN Web Extensions[2]  查看插件開發的事實規範,來仔細研究下:

分爲兩部分

一,manifest 文件關鍵字段釋義

manifest.json 是每個擴展都必須包含的文件,用來指定擴展的元數據,如名字、版本,一些擴展的功能點。這個文件的格式是 jsonc,即可以使用註釋

{
"extension_version": 1 //  這裏是 json 的註釋
}

單純的 JSON 格式不支持註釋語法。重要字段整理如下:

關鍵字段 定義和常用值
default_locale 指定插件文案支持的語言,用來處理國際化
icons 指定在 macOS 和 Safari 各個地方需要顯示的圖標,建議包括 1024 * 1024 尺寸的。目前使用 Xcode 的模板生成的 manifest 文件是不包含的,需要手動加上去。
background background 裏指定在 Safari 運行期間,獨立於頁面生命週期(甚至是獨立於瀏覽器  windows)的 JS代碼,這些代碼可以使用所有 Web Extension APIs 的接口;可以訪問相同的 window 對象。除了最常見的  scripts 字段外,還支持 page 字段,他們兩是互斥的。
content_scripts content_scripts 定義了頁面如何注入 JS 的規則,常見的規則如代碼示例1,指定了哪些頁面注入哪些 JS 文件。這些 JS 會被注入到目標 Web 頁面,去操作目標頁面;同時支持注入 JS 和 CSS(目前不支持 CSS);可以指定 JS 注入的時機,由 run_at 字段來指定;同時支持使用 contentScripts 接口動態註冊
permissions permissions 是向 Safari 瀏覽器申請特定的訪問權限;瀏覽器會幫助我們在用戶界面提示用戶授權。包括三類權限:1. 適用的網頁;2. 可調用 API 範圍;3. activeTab 權限
browser_action browser_action 在瀏覽器上的 toolbar 區域添加一個按鈕,點擊後會打開一個網頁彈窗或者被 background scripts 響應;它的主要目的是處理和具體頁面無關的功能邏輯;
page_action page_action 在瀏覽器上的  URLBar 區域添加一個按鈕(注意和 toolbar 的區別),點擊後會打開一個網頁彈窗或者被 background scripts 響應;主要目的是觸發特定頁面的邏輯;

代碼示例1

"content_scripts": [
  {
    "matches": ["*://*.mozilla.org/*"],
    "js": ["borderify.js"]
  }
]

2. 重要的 API 接口釋義

WebExtensions 的接口,在background scripts,browser scripts,page scripts,sidebars 等頁面裏都可以調用;其中一小部分可以在content scripts裏使用。

越是強大的 APIs,在使用之前越需要獲得用戶的授權,這些授權通常在安裝插件時,會提示用戶。申請權限保持最小範圍原則,越少的權限更容易獲得用戶的授權。

可用 APIs 對象有;activeTab, alarms, background, bookmarks, browserSettings, browsingData, captivePortal, clipboardRead, clipboardWrite, contentSettings, contextMenus, contextualIdentities, cookies, debugger, dns, downloads, downloads.open, find, geolocation, history, identity, idle, management, menus, menus.overrideContext, nativeMessaging, notifications, pageCapture, pkcs11, privacy, proxy, search, sessions, storage, tabHide, tabs, theme, topSites, unlimitedStorage, webNavigation, webRequest, webRequestBlocking,

在 JS 裏調用時,需要調用 browser 命名空間,如

function logTabs(tabs) {
  console.log(tabs)
}
// 這裏 api 調用時,傳入回調的寫法;可用和下面使用 promise 的相比較。
browser.tabs.query({currentWindow: true}, logTabs)

大部分的 APIs 都提供使用 callback 獲取返回值和返回 promise 的兩種方法。把上面的代碼改爲使用 promise 的實現;

function logTabs(tabs) {
  for (let tab of tabs) {
    // tab.url requires the `tabs` permission
    console.log(tab.url);
  }
}

function onError(error) {
  console.log(`Error: ${error}`);
}

let querying = browser.tabs.query({currentWindow: true});
querying.then(logTabs, onError);

注意,chrome 的接口掛載在 chrome 對象下,而且它使用 callback 來作爲異步接口的返回值。在 MDN 的文檔裏,推薦使用 promise 來實現異步接口的返回值;Microsoft Edge 也不支持 promise 的返回值寫法。但如果你要移植到 chrome、IE,需要考慮兼容性問題。

最全的 APIs 接口,請查閱,JavaScript API listing[3]。這裏需要重點介紹一個接口 browser.runtime 系列的接口, 圖例2 裏所示的三方通訊都需要這個接口來實現。

> Extension 各部分如何相互通訊

在實際的插件業務中用到最多的場景是如何在 content scriptsbackground scripts 之前的通訊。根據 MDN 上的接口,我總結了以下的表格,演示其中不同通訊方式的用法。

總結一下,兩部分通訊分爲兩種方式:

  1. 單向,也可以理解爲通知、廣播、多對多。browser.runtime.sendMessage();

  2. 雙向,也可理解爲回調方式、長鏈接,點對點。runtime.port.postMessage()

如何選擇哪種方式,在 MDN 上有給出最佳實踐[4]

一次性消息傳遞和基於連接的消息傳遞之間的選擇取決於您的插件期望如何傳遞這些消息。推薦的考慮是:

選擇一次性消息傳遞。如果...
  • 發一次消息只需要一個回覆

  • 只有個別 script 在監聽一次性消息

選擇基於連接的消息傳遞,如果...
  • 在一個連接中需要多次交換消息

  • 如果擴展需要知道目前任務的進度、是否被中斷,或者想主動中斷任務

另外一部分是 JS 如何和 Native 世界交換數據,我們使用 Session 裏的圖說明。

圖中沒有提到如何從 background 裏向 Native 傳輸數據,因爲不能,需要藉助 Extension ,Extension 也可以直接使用 dispatchMessage 發消息。

隱私和安全

蘋果一貫重視隱私和安全,在插件的 manifest.json 文件聲明的權限是插件默認啓用的權限,在安裝插件時會提示用戶授權;如果是一些某些情況下才需要權限,可以在運行時申請,這時候用戶在觸發操作後會出現授權提示。

插件開發的調試體驗

按照前面描述的插件組成部分,你寫的代碼包括;popup.html 頁面,background.js, content.js如果進入調試頁面呢?

類型 方法
popup.html 點擊工具欄的插件圖標,彈出懸浮窗裏點擊右鍵


background.js 在 Safari 菜單,Develop -> Web Extension Background Pages 裏打開,


content.js 在腳本生效的頁面裏點擊右鍵,喚出審查元素菜單,


當打開調試面板後一切都熟悉起來。這裏建議開發插件時,打開兩個 Safari Window,這樣可以同時調試多個部分的腳本問題。

常見的問題和排除

經過編寫 LightNote 的 demo 插件,發現目前蘋果對 Safari Web Extension 開發工作流做的遠遠不夠。這幾天中,我遇到的常見問題:

  • JS 裏的中文亂碼問題。如果是在 JS 文件裏硬編碼了中文,在別的地方輸出時,就會顯示亂碼。

  • 調試 Native Extension  和 Native App 體驗一如既往的差,Native Extension 經常掛載不上 debugger

  • 修改了 JS,不支持 HotReload 調試,每次修改 ContentScript 都需要重新 run (我後面會重點研究的地方

  • sendNativeMessage()時,傳入 const AppId = "application.id"; 這樣的 AppId 也可以?可見內部實現的時候有很多 magic string 。

  • Safari Web Extension 裏 manifest.json 很多配置都不能如預期工作,在 API 兼容表格里聲稱支持 Content_Script 裏基本參數,但實際上不是。只支持導入一個 JS 文件,多個不行;不支持導入 CSS 文件(大坑)。

  • Safari Web Extension  支持的 MDN Web Extension API 的接口很少,還需要持續完善,請注意辨別。Assessing Your Safari Web Extension’s Browser Compatibility[5] 請收好

  • 我寫的插件 demo 只能在 Xcode 12.2beta3 上運行。12.1 上會報錯,why?

報錯

另外幾個重要的經驗:

  1. 插件的 3 個組成部分,Native Extension, ContentScript, BackgroundScript 。他們的數據流是這樣的:ContentScript 觸發、BackgroundScript 中轉、Native Extension 處理後返回;BackgroundScript 中轉、ContentScript 接收

  2. BackgroundScript 啓動是伴隨瀏覽器啓動,ContentScript 生命週期是和 WebPage 一起的,只有 Native Extension 被請求事件喚醒了,纔會啓動

  3. Swift 裏的 原生對象,如 Dictionary 無縫可以轉化爲 JS 對象,比較方便

  4. 記得調用 API 之前申請權限

  5. ContentScript 裏不能使用 document.onload 之類接口,因爲默認已經在onload 事件之後加載插件 JS 的

  6. 如何適配深色模式。

:root {
    color-scheme: light dark;
}

至此,Session 裏包含的內容講完了。相比其他主流瀏覽器,Safari 的插件生態要差的多。

Safari 插件的生態

在 MDN 上我們看到,Firefox 除了提供開發文檔外,它還提供瞭如發佈、管理、社區等功能,這有一個地方可以讓插件在用戶中流動起來,而相比 Safari 的插件。首先要通過 Mac App 才能安裝;其次它的插件搜索功能實在是太雞肋,封閉,猜測蘋果的想法是如何讓插件也能成爲蘋果新的商業模式,爲他們貢獻現金流。很多插件開發者開發插件很隨意,很 hack,哪些條條框框的約束是對插件開發自由度的一種干涉,雙方不夠協調。導致 Safari 的可用插件少的可憐。在 Safari 插件生態上的建設,多年來停滯不前甚至有倒退。只能期望來年它有些新動作。

本文裏很多 API,會在我提供的 LightNote for Safari 的 demo 裏有所展示,尤其是涵蓋了幾乎所有的通訊方式。這個插件的功能是用戶在打開的頁面上看到不錯的文字。用戶可以高亮標註出來,並且添加評論,可以在一個地方整理。本 demo 只提供雛形,但會在此基礎上迭代,目標是替換 Liner 這個插件,調試插件時注意打開控制檯。

參考資料

[1]

事實標準: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API

[2]

MDN Web Extensions: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API

[3]

JavaScript API listing: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/API

[4]

MDN 上有給出最佳實踐: https://developer.mozilla.org/en-US/docs/Mozilla/Add-ons/WebExtensions/Content_scripts#WebExtension_APIs

[5]

Assessing Your Safari Web Extension’s Browser Compatibility: https://developer.apple.com/documentation/safariservices/safari_web_extensions/assessing_your_safari_web_extension_s_browser_compatibility?language=objc

程序員專欄 掃碼關注填加客服 長按識別下方二維碼進羣

近期精彩內容推薦:  

 再見!深圳!再見!騰訊!

 瘋傳朋友圈的 Pony 馬化騰的講話

 SpringBoot 實現併發登錄人數控制

 異步 Python 比同步 Python 快在哪裏?


在看點這裏好文分享給更多人↓↓

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章