原生訊息傳遞

原生訊息使擴充套件能夠與安裝在使用者計算機上的原生應用程式交換訊息。原生訊息服務於不需要額外 Web 訪問的擴充套件。

密碼管理器:原生應用程式管理、儲存和加密密碼。然後,原生應用程式與擴充套件通訊以填充 Web 表單。

原生訊息還使擴充套件能夠訪問透過 WebExtension API 無法訪問的資源(例如,特定硬體)。

原生應用程式不由瀏覽器安裝或管理。原生應用程式使用底層作業系統的安裝機制進行安裝。建立一個名為“主機清單”或“應用程式清單”的 JSON 檔案。將 JSON 檔案安裝在定義的位置。應用程式清單檔案將描述瀏覽器如何連線到原生應用程式。

擴充套件必須在 manifest.json 檔案中請求 "nativeMessaging" 許可權可選許可權。此外,原生應用程式必須透過在應用程式清單的 "allowed_extensions" 欄位中包含 ID 來授予擴充套件許可權。

安裝後,擴充套件可以與原生應用程式交換 JSON 訊息。使用 runtime API 中的一組函式。在原生應用程式端,訊息透過標準輸入 (stdin) 接收,並透過標準輸出 (stdout) 傳送。

Application flow: the native app JSON file resides on the users computer, providing resource information to the native application. The read and write functions of the native application interact with the browser extension's runtime events.

擴充套件中的原生訊息支援與 Chrome 基本相容,但有兩個主要區別:

  • 應用程式清單將 allowed_extensions 列為應用程式 ID 的陣列,而 Chrome 將 allowed_origins 列為 "chrome-extension" URL 的陣列。
  • 應用程式清單的儲存位置與 Chrome 不同

GitHub 上 webextensions-examples 倉庫的 native-messaging 目錄中有一個完整的示例。本文中的大部分示例程式碼都取自該示例。

設定

擴充套件清單

擴充套件與原生應用程式通訊

manifest.json 檔案示例

json
{
  "description": "Native messaging example add-on",
  "manifest_version": 2,
  "name": "Native messaging example",
  "version": "1.0",
  "icons": {
    "48": "icons/message.svg"
  },

  "browser_specific_settings": {
    "gecko": {
      "id": "ping_pong@example.org",
      "strict_min_version": "50.0"
    }
  },

  "background": {
    "scripts": ["background.js"]
  },

  "browser_action": {
    "default_icon": "icons/message.svg"
  },

  "permissions": ["nativeMessaging"]
}

注意:Chrome 不支援 browser_specific_settings 鍵。您需要使用沒有此鍵的另一個清單才能在 Chrome 上安裝等效的 WebExtension。請參閱下面的 Chrome 不相容性

注意:當使用可選許可權時,請檢查許可權是否已授予,並在必要時,在與原生應用程式通訊之前,使用 permissions API 向用戶請求許可權。

應用程式清單

應用程式清單向瀏覽器描述瞭如何連線到原生應用程式。

應用程式清單檔案必須與原生應用程式一起安裝。瀏覽器讀取和驗證應用程式清單檔案,但不安裝或管理它們。這些檔案的安裝和更新的安全模型更類似於原生應用程式,而不是使用 WebExtension API 的擴充套件。

有關原生應用程式清單語法和位置的詳細資訊,請參閱原生清單

例如,這是 "ping_pong" 原生應用程式的清單:

json
{
  "name": "ping_pong",
  "description": "Example host for native messaging",
  "path": "/path/to/native-messaging/app/ping_pong.py",
  "type": "stdio",
  "allowed_extensions": ["ping_pong@example.org"]
}

這允許 ID 為 "ping_pong@example.org" 的擴充套件透過將名稱 "ping_pong" 傳遞給相關的 runtime API 函式來連線。應用程式本身位於 "/path/to/native-messaging/app/ping_pong.py"

注意:Chrome 使用另一個鍵 allowed_origins 來識別允許的擴充套件,並使用 WebExtension 的 ID。有關更多詳細資訊,請參閱Chrome 文件,並參閱下面的 Chrome 不相容性

Windows 設定

作為示例,您還可以參考 GitHub 上原生訊息擴充套件的 readme。如果您在 Windows 機器上 fork 此儲存庫後想檢查本地設定,可以執行 check_config_win.py 來排除一些問題。

應用程式清單

在上面的示例中,原生應用程式是一個 Python 指令碼。以這種方式讓 Windows 可靠地執行 Python 指令碼可能很困難,因此一種替代方法是提供一個 .bat 檔案,並從應用程式的清單中連結到它。

json
{
  "name": "ping_pong",
  "description": "Example host for native messaging",
  "path": "c:\\path\\to\\native-messaging\\app\\ping_pong_win.bat",
  "type": "stdio",
  "allowed_extensions": ["ping_pong@example.org"]
}

(請參閱上面關於 allowed_extensions 鍵及其在 Chrome 中的對應項的Chrome 相容性的說明)。

批處理檔案然後呼叫 Python 指令碼。

bash
@echo off

python -u "c:\\path\\to\\native-messaging\\app\\ping_pong.py"

登錄檔

瀏覽器根據位於特定位置的登錄檔項查詢擴充套件。您需要使用最終應用程式以程式設計方式新增它們,或者如果您使用 GitHub 上的示例,則手動新增它們。有關更多詳細資訊,請參閱清單位置

ping_pong 示例為例,如果使用 Firefox(參閱此頁面瞭解 Chrome),則應建立兩個登錄檔項之一以使訊息傳遞工作:

  • HKEY_CURRENT_USER\Software\Mozilla\NativeMessagingHosts\ping_pong
  • HKEY_LOCAL_MACHINE\Software\Mozilla\NativeMessagingHosts\ping_pong

該鍵的預設值應為應用程式清單的路徑:例如 C:\Users\<myusername>\webextensions-examples\native-messaging\app\ping_pong.json

注意:如果您基於 GitHub 上的示例進行工作,請閱讀readme 的這一部分,並在瀏覽器上安裝 WebExtension 之前檢查 check_config_win.py 的輸出。

交換訊息

有了上述設定,擴充套件就可以與原生應用程式交換 JSON 訊息。

擴充套件端

原生訊息不能直接在內容指令碼中使用。您必須通過後臺指令碼間接進行

這裡有兩種模式可以使用:基於連線的訊息傳遞無連線訊息傳遞

基於連線的訊息傳遞

使用此模式,您呼叫 runtime.connectNative(),傳入應用程式的名稱(應用程式清單中 "name" 屬性的值)。如果應用程式尚未執行,這將啟動應用程式並向擴充套件返回一個 runtime.Port 物件。

應用程式啟動時會傳入兩個引數:

  • 應用程式清單的完整路徑。
  • (Firefox 55 新增)啟動它的附加元件的 ID(在 browser_specific_settings manifest.json 鍵中給出)。

注意:Chrome 對傳入引數的處理方式不同:

  • 在 Linux 和 Mac 上,Chrome 傳入一個引數:啟動它的擴充套件的來源(格式為 chrome-extension://[extensionID])。這使得應用程式能夠識別擴充套件。
  • 在 Windows 上,Chrome 傳入兩個引數:第一個是擴充套件的來源,第二個是啟動應用程式的 Chrome 原生視窗的控制代碼。

應用程式保持執行,直到擴充套件呼叫 Port.disconnect() 或連線到它的頁面被關閉。

要使用 Port 傳送訊息,請呼叫其 postMessage() 函式,傳入要傳送的 JSON 訊息。要使用 Port 監聽訊息,請使用其 onMessage.addListener() 函式新增監聽器。

這是一個示例後臺指令碼,它與 "ping_pong" 應用程式建立連線,監聽來自它的訊息,然後在使用者點選瀏覽器操作時向它傳送 "ping" 訊息:

js
/*
On startup, connect to the "ping_pong" app.
*/
let port = browser.runtime.connectNative("ping_pong");

/*
Listen for messages from the app.
*/
port.onMessage.addListener((response) => {
  console.log(`Received: ${response}`);
});

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  console.log("Sending:  ping");
  port.postMessage("ping");
});

無連線訊息傳遞

使用此模式,您呼叫 runtime.sendNativeMessage(),傳入:

  • 應用程式的名稱
  • 要傳送的 JSON 訊息
  • 可選的回撥函式。

每條訊息都會建立一個新的應用程式例項。應用程式啟動時會傳入兩個引數:

  • 應用程式清單的完整路徑
  • (Firefox 55 新增)啟動它的附加元件的 ID(在 browser_specific_settings manifest.json 鍵中給出)。

應用程式傳送的第一條訊息被視為對 sendNativeMessage() 呼叫的響應,並將傳遞給回撥函式。

這是上面的示例,使用 runtime.sendNativeMessage() 重寫:

js
function onResponse(response) {
  console.log(`Received ${response}`);
}

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

/*
On a click on the browser action, send the app a message.
*/
browser.browserAction.onClicked.addListener(() => {
  console.log("Sending:  ping");
  let sending = browser.runtime.sendNativeMessage("ping_pong", "ping");
  sending.then(onResponse, onError);
});

應用程式端

在應用程式端,您使用標準輸入接收訊息,並使用標準輸出傳送訊息。

每條訊息都使用 JSON 序列化,UTF-8 編碼,並以一個無符號的 32 位值開頭,該值包含訊息長度(以原生位元組順序)。

應用程式發出的單條訊息的最大大小為 1 MB。傳送到應用程式的訊息的最大大小為 4 GB。

您可以使用此 NodeJS 程式碼 nm_nodejs.mjs 快速開始傳送和接收訊息:

js
#!/usr/bin/env -S /full/path/to/node

import fs from "node:fs/promises";

async function getMessage() {
  const header = new Uint32Array(1);
  await readFullAsync(1, header);
  const message = await readFullAsync(header[0]);
  return message;
}

async function readFullAsync(length, buffer = new Uint8Array(65536)) {
  const data = [];
  while (data.length < length) {
    const input = await fs.open("/dev/stdin");
    const { bytesRead } = await input.read({ buffer });
    await input.close();
    if (bytesRead === 0) {
      break;
    }
    data.push(...buffer.subarray(0, bytesRead));
  }
  return new Uint8Array(data);
}

async function sendMessage(message) {
  const header = Buffer.from(new Uint32Array([message.length]).buffer);
  const stdout = process.stdout;
  await stdout.write(header);
  await stdout.write(message);
}

while (true) {
  try {
    const message = await getMessage();
    await sendMessage(message);
  } catch (e) {
    console.error(e);
    process.exit(1);
  }
}

這是用 Python 編寫的另一個示例。它監聽來自擴充套件的訊息。請注意,該檔案在 Linux 上必須是可執行的。如果訊息是 "ping",則它會響應訊息 "pong"

這是 Python 2 版本:

python
#!/usr/bin/env -S python2 -u

# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import json
import sys
import struct

# Read a message from stdin and decode it.
def get_message():
    raw_length = sys.stdin.read(4)
    if not raw_length:
        sys.exit(0)
    message_length = struct.unpack('=I', raw_length)[0]
    message = sys.stdin.read(message_length)
    return json.loads(message)

# Encode a message for transmission, given its content.
def encode_message(message_content):
    # https://docs.python.club.tw/3/library/json.html#basic-usage
    # To get the most compact JSON representation, you should specify
    # (',', ':') to eliminate whitespace.
    # We want the most compact representation because the browser rejects
    # messages that exceed 1 MB.
    encoded_content = json.dumps(message_content, separators=(',', ':'))
    encoded_length = struct.pack('=I', len(encoded_content))
    return {'length': encoded_length, 'content': encoded_content}

# Send an encoded message to stdout.
def send_message(encoded_message):
    sys.stdout.write(encoded_message['length'])
    sys.stdout.write(encoded_message['content'])
    sys.stdout.flush()

while True:
    message = get_message()
    if message == "ping":
        send_message(encode_message("pong"))

在 Python 3 中,接收到的二進位制資料必須解碼為字串。要傳送回附加元件的內容必須使用結構體編碼為二進位制資料:

python
#!/usr/bin/env -S python3 -u

# Note that running python with the `-u` flag is required on Windows,
# in order to ensure that stdin and stdout are opened in binary, rather
# than text, mode.

import sys
import json
import struct

# Read a message from stdin and decode it.
def getMessage():
    rawLength = sys.stdin.buffer.read(4)
    if len(rawLength) == 0:
        sys.exit(0)
    messageLength = struct.unpack('@I', rawLength)[0]
    message = sys.stdin.buffer.read(messageLength).decode('utf-8')
    return json.loads(message)

# Encode a message for transmission,
# given its content.
def encodeMessage(messageContent):
    # https://docs.python.club.tw/3/library/json.html#basic-usage
    # To get the most compact JSON representation, you should specify
    # (',', ':') to eliminate whitespace.
    # We want the most compact representation because the browser rejects # messages that exceed 1 MB.
    encodedContent = json.dumps(messageContent, separators=(',', ':')).encode('utf-8')
    encodedLength = struct.pack('@I', len(encodedContent))
    return {'length': encodedLength, 'content': encodedContent}

# Send an encoded message to stdout
def sendMessage(encodedMessage):
    sys.stdout.buffer.write(encodedMessage['length'])
    sys.stdout.buffer.write(encodedMessage['content'])
    sys.stdout.buffer.flush()

while True:
    receivedMessage = getMessage()
    if receivedMessage == "ping":
        sendMessage(encodeMessage("pong"))

關閉原生應用程式

如果您使用 runtime.connectNative() 連線到原生應用程式,那麼它將保持執行,直到擴充套件呼叫 Port.disconnect() 或連線到它的頁面被關閉。如果您透過傳送 runtime.sendNativeMessage() 啟動原生應用程式,那麼它將在收到訊息併發送響應後關閉。

關閉原生應用程式

  • 在 macOS 和 Linux 等類 Unix 系統上,瀏覽器會向原生應用程式傳送 SIGTERM,然後在應用程式有機會優雅退出後傳送 SIGKILL。除非子程序脫離到新的程序組中,否則這些訊號會傳播到任何子程序。
  • 在 Windows 上,瀏覽器將原生應用程式的程序放入 作業物件 並終止該作業。如果原生應用程式啟動了其他程序並希望它們在原生應用程式被終止後保持開啟,那麼原生應用程式必須使用 CREATE_BREAKAWAY_FROM_JOB 標誌啟動其他程序,例如透過使用 CreateProcess

故障排除

如果出現問題,請檢查瀏覽器控制檯。如果原生應用程式向 stderr 傳送任何輸出,瀏覽器會將其重定向到瀏覽器控制檯。因此,如果您已經啟動了原生應用程式,您將看到它發出的任何錯誤訊息。

如果您未能執行應用程式,您應該會看到一條錯誤訊息,為您提供有關問題的線索。

"No such native application <name>"
  • 檢查傳遞給 runtime.connectNative() 的名稱是否與應用程式清單中的名稱匹配。

  • macOS/Linux:檢查應用程式清單的名稱是否為 <name>.json

  • macOS/Linux:檢查原生應用程式的清單檔案位置,如原生清單參考中所述。

  • Windows:檢查登錄檔項是否在正確的位置,並且其名稱是否與應用程式清單中的名稱匹配。

  • Windows:檢查登錄檔項中給出的路徑是否指向應用程式清單。

    "Error: Invalid application <name>"
    
  • 檢查應用程式的名稱是否不包含無效字元。

    "'python' is not recognized as an internal or external command, ..."
    
  • Windows:如果您的應用程式是 Python 指令碼,請檢查您是否安裝了 Python 併為其設定了路徑。

    "File at path <path> does not exist, or is not executable"
    
  • 如果您看到此訊息,則表示已成功找到應用程式清單。

  • 檢查應用程式清單中的“path”是否正確。

  • Windows:檢查您是否轉義了路徑分隔符("c:\\path\\to\\file")。

  • 檢查應用程式是否位於應用程式清單中 "path" 屬性指向的位置。

  • 檢查應用程式是否可執行。

    "This extension does not have permission to use native application <name>"
    
  • 檢查應用程式清單中的 "allowed_extensions" 鍵是否包含附加元件的 ID。

        "TypeError: browser.runtime.connectNative is not a function"
    
  • 檢查擴充套件是否具有 "nativeMessaging" 許可權。

    "[object Object]       NativeMessaging.jsm:218"
    
  • 啟動應用程式時出現問題。

Chrome 不相容性

在 Web 擴充套件中,原生訊息傳遞受到瀏覽器之間許多差異的影響,包括傳遞給原生應用程式的引數、清單檔案的位置等。這些差異在Chrome 不相容性 > 原生訊息傳遞中進行了討論。