透過 JavaScript 橋接器存取原生 API

本頁將討論建立原生橋接器 (又稱 JavaScript 橋接器) 的各種方法和最佳做法,以利 WebView 中的網頁內容與主機 Android 應用程式之間的通訊。

這項功能可讓網頁開發人員使用 JavaScript 存取原生平台功能,例如攝影機、檔案系統或進階硬體感應器,這些功能通常不屬於標準網頁 API。

用途

JavaScript 橋接器實作項目可支援各種整合情境,讓網路內容更深入存取 Android 作業系統。以下列舉幾個範例:

  • 平台整合:從網頁觸發原生 Android UI 元件 (例如生物特徵辨識提示、BottomSheetDialog)。
  • 效能:將大量運算工作卸載至原生 Java 或 Kotlin 程式碼。
  • 資料持久性:存取本機加密資料庫或共用偏好設定。
  • 大型資料轉移:在應用程式和網頁算繪器之間傳遞媒體檔案或複雜的資料結構。

通訊機制

Android 提供三種主要世代的 API,可建立原生橋接器。 雖然這些方法仍可使用,但安全性、可用性和效能差異很大。

使用 addWebMessageListener (建議)

addWebMessageListener 是最先進且建議採用的方法,可讓網頁內容與原生應用程式程式碼進行通訊。這項服務結合了 JavaScript 介面的易用性與訊息系統的安全性。

運作方式:應用程式會新增具有特定名稱和一組允許來源規則的接聽程式。接著,WebView 會確保 JavaScript 物件從網頁開始載入時,就存在於全域範圍 (window.objectName) 中。

初始化:為確保 WebView 在任何指令碼執行前插入 JavaScript 物件,您必須先呼叫 addWebMessageListener,再呼叫 loadUrl()

主要功能

  • 安全與信任:與舊版 API 不同,這個方法在初始化期間需要 allowedOriginRulesSet<String>。這是建立信任感的主要機制。

    指定信任來源 (例如 https://example.com) 時,WebView 會確保只向從該確切來源載入的網頁公開插入的 JavaScript 物件。

    原生事件監聽器回呼會在每則訊息中收到 sourceOrigin 參數。如果橋接器支援多個允許的來源,您可以使用這項資訊驗證寄件者的確切來源。

    由於 WebView 會在平台層級嚴格執行這些來源檢查,因此您的應用程式通常可以信任從 sourceOrigin 收到的訊息,不必在大多數標準實作中進行嚴格的酬載驗證。

    • WebView 會根據通訊協定 (HTTP/HTTPS)、主機和通訊埠比對規則。
    • WebView 會忽略路徑。例如,https://example.com 允許 https://example.com/loginhttps://example.com/home
    • WebView 會嚴格限制萬用字元只能用於子網域主機的開頭。舉例來說,https://*.example.com 符合 https://foo.example.com,但不符合 https://example.com。如要比對 https://example.com 和子網域,必須分別將每個原始規則新增至許可清單 (例如 "https://example.com", "https://*.example.com")。您無法使用萬用字元做為架構,或在網域中間使用萬用字元。

    這項限制可確保只有經過驗證的網域能使用橋接器,防止未經授權的第三方內容或插入的 iFrame 執行原生程式碼。

  • 支援多個框架:適用於符合來源規則的所有框架。

  • 執行緒:監聽器回呼會在應用程式的主要 (UI) 執行緒上執行。如果橋接器需要處理複雜的資料處理、JSON 剖析或資料庫查詢作業,您必須將這些工作卸載至背景執行緒,以免應用程式 UI 凍結,並發生「應用程式無回應」(ANR) 錯誤。

  • 雙向:網頁傳送訊息時,應用程式會收到 JavaScriptReplyProxy,可用於將訊息傳回該特定影格。您可以保留這個 replyProxy 物件,隨時用來傳送任意數量的訊息給粉絲專頁,而不只是回覆粉絲專頁傳送的每則訊息。如果原始影格導覽離開或遭到毀損,系統會自動忽略透過 Proxy 傳送的 postMessage() 訊息。

  • 應用程式端啟動:雖然網頁一律必須啟動與應用程式的通訊管道,但原生應用程式可以單方面提示網頁開始這個程序。原生應用程式可透過 addDocumentStartJavaScript() (在網頁載入前評估 JavaScript) 或 evaluateJavaScript() (在網頁載入後評估 JavaScript) 與網頁通訊。

限制:這個 API 會以字串或 byte[] 陣列的形式傳送資料。如果是 JSON 物件等較複雜的資料結構,您必須將其序列化為其中一種格式,然後在另一端還原序列化,以重建資料結構。

使用範例

如要瞭解雙向訊息交換的完整順序,請依下列順序進行事件:

  1. 啟動 (應用程式):原生應用程式會使用 addWebMessageListener 註冊事件監聽器,並透過 loadUrl() 載入網頁。
  2. 傳送訊息 (網頁):網頁的 JavaScript 會呼叫 myObject.postMessage(message) 來啟動通訊。
  3. 接收及回覆訊息 (應用程式):應用程式會在接聽程式回呼中接收訊息,並使用提供的 replyProxy.postMessage() 回覆。
  4. 接收回覆 (網頁):網頁會在 myObject.onmessage() 回呼函式中收到非同步回覆。

Kotlin

val myListener = WebViewCompat.WebMessageListener { _, _, _, _, replyProxy ->
    // Handle the message from JS
    replyProxy.postMessage("Acknowledged!")
}

// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
    val allowedOrigins = setOf("https://www.example.com")
    WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener)
}

Java

WebMessageListener myListener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {
    // Handle the message from JS
    replyProxy.postMessage("Acknowledged!");
};

// Check whether the WebView version supports the feature.
if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
    Set<String> allowedOrigins = Set.of("https://www.example.com");
    WebViewCompat.addWebMessageListener(webView, "myObject", allowedOrigins, myListener);
}

下列 JavaScript 示範 addWebMessageListener 的用戶端實作方式,可讓網頁內容透過 myObject 代理程式接收來自原生應用程式的訊息,並傳送自己的訊息。

myObject.onmessage = function(event) {
    console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");

使用 postWebMessage (替代方案)

Android 推出這項功能,是為了提供類似網頁 window.postMessage 的非同步訊息傳遞替代方案。

運作方式:應用程式會使用 WebViewCompat.postWebMessage 將酬載傳送至網頁的主要框架。如要建立雙向通訊管道,可以建立 WebMessageChannel,並將其中一個通訊埠連同訊息傳遞至網頁內容。

特徵

  • 非同步:與 addWebMessageListener 類似,這個方法會使用非同步訊息傳遞,確保網頁在應用程式於背景處理資料時,仍能回應使用者互動。
  • 可辨識來源:您可以指定 targetOrigin,確保 WebView 只會將資料傳送至信任的網站。

限制

  • 範圍:這項 API 會將通訊限制在主要框架內。不支援直接傳送訊息至 iframe 或在 iframe 中傳送訊息。
  • URI 限制:您無法對使用 data: URI、file: URI 或 loadData() 載入的內容使用這個方法,除非您將「*」指定為目標來源。這樣一來,任何網頁都能接收訊息。
  • 身分風險:網頁內容無法清楚驗證傳送者的身分。網頁收到的訊息可能來自原生應用程式或其他 iframe。

如果 Android 版本較舊,不支援 addWebMessageListener,且您需要簡單的字串資料非同步管道,請使用這個方法。

使用「addJavascriptInterface」(舊版)

最舊的方法是將原生物件例項直接注入 WebView。

運作方式:定義 Kotlin 或 Java 類別,使用 @JavascriptInterface 註解允許的方法,然後使用 addJavascriptInterface(Object, String) 將類別例項新增至 WebView。

特徵

  • 同步:JavaScript 執行環境會封鎖,直到 Android 程式碼中的方法傳回為止。
  • 執行緒安全:系統會在背景執行緒上呼叫方法,因此 Kotlin 或 Java 端必須謹慎同步處理。
  • 安全風險:根據預設,WebView 中的每個影格 (包括 iframe) 都能使用 addJavascriptInterface。缺少以來源為準的存取控管機制。由於 WebView 的非同步行為,無法安全地判斷呼叫介面的影格網址。您不得依賴 WebView.getUrl() 等方法進行安全性驗證,因為這些方法無法保證準確性,且不會指出發出要求的特定影格。

機制摘要

下表快速比較三種主要的原生橋接器實作機制:

方法 addWebMessageListener postWebMessage addJavascriptInterface
實作 非同步 (主執行緒上的接聽程式) 非同步 同步
安全性 最高 (以許可清單為準) 高 (來源感知) 低 (不檢查來源)
複雜度 簡潔
方向 雙向 雙向 網站到應用程式
最低 WebView 版本 82 版 (和 Jetpack Webkit 1.3.0) 版本 45 (和 Jetpack Webkit 1.1.0) 所有版本
推薦項目

處理大型資料移轉作業

傳輸大型酬載 (例如數 MB 的字串或二進位檔案) 時,您必須謹慎管理記憶體,以免 32 位元裝置發生應用程式無回應 (ANR) 錯誤或當機。本節將討論在主機應用程式和網頁內容之間傳輸大量資料時,相關的各種技術和限制。

使用位元組陣列傳輸二進位資料

使用 WebMessageCompat 類別時,您可以直接傳送 byte[] 陣列,不必將二進位資料序列化為 Base64 字串。由於 Base64 會為資料大小增加約 33% 的額外負荷,因此這種做法的記憶體效率更高,速度也更快。

  • 二進位優勢:在原生應用程式和網頁內容之間傳輸二進位資料,例如圖片檔案或音訊。
  • 限制:即使使用位元組陣列,系統也會在應用程式與 WebView 用於算繪網頁內容的隔離程序之間,跨程序間通訊 (IPC) 邊界複製資料。但如果檔案非常大,仍會耗用大量記憶體。

下列程式碼範例說明如何在原生應用程式端設定 addWebMessageListener,接收標示 WebMessageCompat.TYPE_ARRAY_BUFFER 的訊息,並視需要檢查 WebViewFeature.MESSAGE_ARRAY_BUFFER,以二進位資料回覆。

Kotlin

fun setupWebView(webView: WebView) {
  if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
      val listener = WebViewCompat.WebMessageListener { view, message, sourceOrigin, isMainFrame, replyProxy ->

          // Check if the received message is an ArrayBuffer
          if (message.type == WebMessageCompat.TYPE_ARRAY_BUFFER) {
              val binaryData: ByteArray = message.arrayBuffer
              // Process your binary data (image, audio, etc.)
              println("Received bytes: ${binaryData.size}")

              // Optional: Send a binary reply back to JavaScript.
              // This example sends a 3-byte array for simplicity.
              if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
                  val replyBytes = byteArrayOf(0x01, 0x02, 0x03)
                  replyProxy.postMessage(replyBytes)
              }
          }
      }

      // "myBridge" matches the window.myBridge in JavaScript
      WebViewCompat.addWebMessageListener(
          webView,
          "myBridge",
          setOf("https://example.com"), // Security: restrict origins
          listener
      )
  }
}

Java

public void setupWebView(WebView webView) {
  if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_LISTENER)) {
      WebViewCompat.WebMessageListener listener = (view, message, sourceOrigin, isMainFrame, replyProxy) -> {

          // Check if the received message is an ArrayBuffer
          if (message.getType() == WebMessageCompat.TYPE_ARRAY_BUFFER) {
              byte[] binaryData = message.getArrayBuffer();
              // Process your binary data (image, audio, etc.)
              System.out.println("Received bytes: " + binaryData.length);

              // Optional: Send a binary reply back to JavaScript.
              // This example sends a 3-byte array for simplicity.
              if (WebViewFeature.isFeatureSupported(WebViewFeature.WEB_MESSAGE_ARRAY_BUFFER)) {
                  byte[] replyBytes = new byte[]{0x01, 0x02, 0x03};
                  replyProxy.postMessage(replyBytes);
              }
          }
      };

      // "myBridge" matches the window.myBridge in JavaScript
      WebViewCompat.addWebMessageListener(
          webView,
          "myBridge",
          Set.of("https://example.com"), // Security: restrict origins
          listener
      );
  }
}

下列 JavaScript 程式碼示範了 addWebMessageListener 的用戶端實作方式,可讓網頁內容使用上一個範例中插入的 window.myBridge 代理程式,與原生應用程式來回傳送及接收二進位資料 (ArrayBuffer)。

// Function to send an image or binary buffer to the app
async function sendBinaryToApp() {
    const response = await fetch('image.jpg');
    const buffer = await response.arrayBuffer();

    // Check if the injected bridge object exists
    if (window.myBridge) {
        // You can send the ArrayBuffer directly
        window.myBridge.postMessage(buffer);
    }
}

// Receiving binary data from the app
if (window.myBridge) {
    window.myBridge.onmessage = function(event) {
        if (event.data instanceof ArrayBuffer) {
            console.log('Received binary data from App, length:', event.data.byteLength);
            // Process the binary data (for example, as a Uint8Array)
            const bytes = new Uint8Array(event.data);
            console.log('First byte:', bytes[0]);
        }
    };
}

有效率地載入大量資料

如果是非常大的檔案 (超過 10 MB),請使用 shouldInterceptRequest 方法串流資料:

  1. 網頁會對自訂預留位置網址發出 fetch() 呼叫。例如:https://app.local/large-file
  2. Android 應用程式會在 WebViewClient.shouldInterceptRequest 中攔截這項要求。
  3. 應用程式會以 InputStream 形式傳回資料。

這樣一來,您就能以區塊形式串流資料,不必一次將整個酬載載入記憶體。

下列 JavaScript 函式示範用戶端程式碼,可使用標準 fetch() 呼叫自訂預留位置網址,從原生應用程式有效率地載入大型二進位檔案。

async function fetchBinaryFromApp() {
    try {
        // This URL doesn't need to exist on the internet
        const response = await fetch('https://app.local/data/large-file.bin');

        if (!response.ok) throw new Error('Network response was not okay');

        // For raw binary data:
        const arrayBuffer = await response.arrayBuffer();
        console.log('Received binary data, size:', arrayBuffer.byteLength);
        // Process buffer (for example, new Uint8Array(arrayBuffer))

        /*
        // OR for an image:
        const blob = await response.blob();
        const imageUrl = URL.createObjectURL(blob);
        document.getElementById('myImage').src = imageUrl;
        */

    } catch (error) {
        console.error('Fetch error:', error);
    }
}

下列程式碼範例示範原生應用程式端,如何使用 Kotlin 和 Java 中的 WebViewClient.shouldInterceptRequest 方法,攔截網頁內容要求的自訂預留位置網址,藉此串流大型二進位檔案。

Kotlin

webView.webViewClient = object : WebViewClient() {
  override fun shouldInterceptRequest(
      view: WebView?,
      request: WebResourceRequest?
  ): WebResourceResponse? {
      val url = request?.url ?: return null

      // Check if this is our custom placeholder URL
      if (url.host == "app.local" && url.path == "/data/large-file.bin") {
          try {
              // 1. Get your data as an InputStream
              // (from Assets, Files, or a generated byte stream)
              val inputStream: InputStream = context.assets.open("my_data.pb")

              // 2. Define Response Headers (Crucial for CORS/Fetch)
              val headers = mutableMapOf<String, String>()
              headers["Access-Control-Allow-Origin"] = "*" // Allow fetch from any origin

              // 3. Return the response
              return WebResourceResponse(
                  "application/octet-stream", // MIME type (for example, image/jpeg)
                  "UTF-8",                   // Encoding
                  200,                       // Status Code
                  "OK",                      // Reason Phrase
                  headers,                   // Custom Headers
                  inputStream                // The actual data stream
              )
          } catch (e: Exception) {
              // Handle exception
          }
      }
      return super.shouldInterceptRequest(view, request)
  }
}

Java

webView.setWebViewClient(new WebViewClient() {
  @Override
  public WebResourceResponse shouldInterceptRequest(WebView view, WebResourceRequest request) {
      String urlPath = request.getUrl().getPath();
      String host = request.getUrl().getHost();

      // Check if this is our custom placeholder URL
      if ("app.local".equals(host) && "/data/large-file.bin".equals(urlPath)) {
          try {
              // 1. Get your data as an InputStream
              // (from Assets, Files, or a generated byte stream)
              InputStream inputStream = getContext().getAssets().open("my_data.pb");

              // 2. Define Response Headers (Crucial for CORS/Fetch)
              Map<String, String> headers = new HashMap<>();
              headers.put("Access-Control-Allow-Origin", "*"); // Allow fetch from any origin

              // 3. Return the response
              return new WebResourceResponse(
                  "application/octet-stream", // MIME type (for example, image/jpeg)
                  "UTF-8",                   // Encoding
                  200,                       // Status Code
                  "OK",                      // Reason Phrase
                  headers,                   // Custom Headers
                  inputStream                // The actual data stream
              );
          } catch (Exception e) {
              // Handle exception
          }
      }
      return super.shouldInterceptRequest(view, request);
  }
});

依循安全性建議

為保護應用程式和使用者資料,實作橋接器時請遵循下列規範:

  • 強制執行 HTTPS:為確保惡意第三方內容無法叫用應用程式的原生邏輯,請僅允許與安全來源通訊。

  • 依據來源規則:處理信任問題的最佳方式,就是嚴格定義 allowedOriginRules,並檢查訊息回呼中提供的 sourceOrigin。除非絕對必要,否則請避免使用完整萬用字元 (*) 做為唯一的來源規則,因為這會比對所有來源。使用子網域的萬用字元 (例如 *.example.com) 仍有效且安全,可比對多個子網域 (例如 foo.example.combar.example.com)。

    注意:來源規則可防範惡意第三方網站和隱藏的 iframe,但無法防範您信任網域內的跨網站指令碼 (XSS) 安全漏洞。舉例來說,如果網頁顯示使用者自製內容,且容易遭受儲存型 XSS 攻擊,攻擊者就能執行指令碼,假冒您信任的來源。執行敏感的原生平台作業前,請考慮對訊息酬載套用驗證。

  • 盡量減少公開的內容:只公開網頁所需的方法或資料。

  • 在執行階段檢查功能:包括 addWebMessageListener 在內,最近的 Bridge API 屬於 Jetpack Webkit 程式庫。因此,請務必先使用 WebViewFeature.isFeatureSupported() 檢查是否支援,再撥打電話。