Dostęp do natywnych interfejsów API za pomocą mostu JavaScript

Na tej stronie omawiamy różne metody i sprawdzone praktyki tworzenia mostu natywnego, zwanego też mostem JavaScript, który ułatwia komunikację między treściami internetowymi w WebView a aplikacją hostującą na Androida.

Umożliwia to programistom internetowym korzystanie z JavaScriptu w celu uzyskania dostępu do funkcji platformy natywnej, takich jak aparat, system plików czy zaawansowane czujniki sprzętowe, których standardowe interfejsy API zwykle nie udostępniają.

Przypadki użycia

Implementacja mostu JavaScript umożliwia różne scenariusze integracji, w których treści internetowe wymagają głębszego dostępu do systemu operacyjnego Android. Oto kilka przykładów:

  • Integracja z platformą: wywoływanie natywnych komponentów interfejsu Androida (np. komunikatów biometrycznych, BottomSheetDialog) ze strony internetowej.
  • Wydajność: przenoszenie wymagających obliczeniowo zadań do natywnego kodu Java lub Kotlin.
  • Trwałość danych: dostęp do lokalnych zaszyfrowanych baz danych lub udostępnionych preferencji.
  • Przesyłanie dużych ilości danych: przekazywanie plików multimedialnych lub złożonych struktur danych między aplikacją a renderowaniem w internecie.

Mechanizmy komunikacji

Android oferuje 3 główne generacje interfejsów API do tworzenia natywnego mostu. Wszystkie są nadal dostępne, ale różnią się znacznie pod względem bezpieczeństwa, użyteczności i wydajności.

Użyj addWebMessageListener (zalecane)

addWebMessageListener to najnowocześniejsze i zalecane podejście do komunikacji między treściami internetowymi a kodem aplikacji natywnej. Łączy on łatwość obsługi interfejsu JavaScript z bezpieczeństwem systemu przesyłania wiadomości.

Jak to działa: aplikacja dodaje odbiornik o określonej nazwie i zestawie reguł dozwolonych źródeł. Komponent WebView dba o to, aby obiekt JavaScript był obecny w zakresie globalnym (window.objectName) od momentu rozpoczęcia wczytywania strony.

Inicjowanie: aby mieć pewność, że komponent WebView wstrzyknie obiekt JavaScript przed uruchomieniem jakiegokolwiek skryptu, musisz wywołać metodę addWebMessageListener przed przejściem do strony (np. wywołując WebViewCompat.navigate lub loadUrl).

Najważniejsze funkcje:

  • Bezpieczeństwo i zaufanie: w przeciwieństwie do starszych interfejsów API ta metoda wymaga Set<String>allowedOriginRules podczas inicjowania. To główny mechanizm budowania zaufania.

    Gdy określisz zaufane źródło, np. https://example.com, komponent WebView gwarantuje, że udostępni wstrzyknięte obiekty JavaScript tylko stronom internetowym wczytanym z tego źródła.

    Wywołanie zwrotne odbiornika natywnego otrzymuje parametr sourceOrigin z każdą wiadomością. Możesz użyć tej opcji, aby sprawdzić dokładne pochodzenie nadawcy, jeśli Twój most obsługuje wiele dozwolonych źródeł.

    Ponieważ komponent WebView ściśle egzekwuje te weryfikacje pochodzenia na poziomie platformy, aplikacja może zwykle polegać na wiadomościach otrzymywanych z zaufanego źródłasourceOrigin, co eliminuje potrzebę rygorystycznej weryfikacji ładunku w większości standardowych implementacji.

    • WebView dopasowuje reguły do schematu (HTTP/HTTPS), hosta i portu.
    • WebView ignoruje ścieżki. Na przykład https://example.com zezwala na https://example.com/loginhttps://example.com/home.
    • WebView ściśle ogranicza symbole wieloznaczne do początku hosta w przypadku subdomen. Na przykład wzorzec https://*.example.com pasuje do https://foo.example.com, ale nie do https://example.com. Jeśli chcesz dopasować zarówno https://example.com, jak i jego subdomeny, musisz dodać każdą regułę pochodzenia osobno do listy dozwolonych (np. "https://example.com", "https://*.example.com"). Nie możesz używać symboli wieloznacznych w schemacie ani w środku domeny.

    Ogranicza to działanie mostu do zweryfikowanych domen, co uniemożliwia wykonywanie kodu natywnego przez nieautoryzowane treści osób trzecich lub wstrzyknięte elementy iframe.

  • Obsługa wielu ramek: działa we wszystkich ramkach, które pasują do reguł dotyczących pochodzenia.

  • Wątkowość: wywołanie zwrotne odbiornika jest wykonywane w głównym wątku aplikacji (UI). Jeśli most musi obsługiwać złożone przetwarzanie danych, analizowanie JSON-a lub wyszukiwanie w bazie danych, musisz przenieść to zadanie do wątku w tle, aby zapobiec zawieszaniu interfejsu aplikacji z błędem „aplikacja nie odpowiada” (ANR).

  • Dwukierunkowe: gdy strona wysyła wiadomość, aplikacja otrzymuje JavaScriptReplyProxy, którego może użyć do wysyłania wiadomości z powrotem do tej konkretnej ramki. Możesz zachować ten obiekt replyProxy i używać go w dowolnym momencie, aby wysyłać dowolną liczbę wiadomości na stronę, a nie tylko odpowiadać na każdą wiadomość wysłaną przez stronę. Jeśli ramka źródłowa zostanie zamknięta lub zniszczona, wiadomości wysłane za pomocą postMessage() w serwerze proxy będą ignorowane.

  • Inicjowanie po stronie aplikacji: chociaż strona internetowa musi zawsze inicjować kanał komunikacji z aplikacją, aplikacja natywna może jednostronnie poprosić stronę internetową o rozpoczęcie tego procesu. Aplikacja natywna może komunikować się ze stroną internetową za pomocą funkcji addDocumentStartJavaScript() (aby ocenić kod JavaScript przed załadowaniem strony) lub evaluateJavaScript() (aby ocenić kod JavaScript po załadowaniu strony).

Ograniczenie: ten interfejs API przesyła dane w postaci ciągów znaków lub tablic byte[]. W przypadku bardziej złożonych struktur danych, takich jak obiekty JSON, musisz je serializować do jednego z tych formatów, a następnie deserializować po drugiej stronie, aby odtworzyć strukturę danych.

Przykład zastosowania:

Aby poznać pełną sekwencję dwukierunkowej wymiany wiadomości, zdarzenia przebiegają w tej kolejności:

  1. Inicjowanie (aplikacja): aplikacja natywna rejestruje odbiorcę za pomocą funkcji addWebMessageListener i inicjuje nawigację po stronie (np. za pomocą funkcji WebViewCompat.navigate lub loadUrl).
  2. Wysłanie wiadomości (w internecie): JavaScript na stronie internetowej wywołuje myObject.postMessage(message), aby zainicjować komunikację.
  3. Odbieranie wiadomości i odpowiadanie na nie (aplikacja): aplikacja odbiera wiadomość w wywołaniu zwrotnym odbiornika i odpowiada na nią za pomocą podanego parametru replyProxy.postMessage().
  4. Otrzymanie odpowiedzi (internet): strona internetowa otrzymuje odpowiedź asynchroniczną w funkcji wywołania zwrotnego 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);
}

Poniższy kod JavaScript pokazuje implementację po stronie klienta interfejsu addWebMessageListener, który umożliwia treściom internetowym odbieranie wiadomości z aplikacji natywnej i wysyłanie własnych wiadomości za pomocą serwera proxy myObject.

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

Użyj postWebMessage (alternatywnie)

Android wprowadził to rozwiązanie, aby zapewnić asynchroniczną alternatywę opartą na przesyłaniu wiadomości, podobną do window.postMessage w internecie.

Jak to działa: aplikacja używa WebViewCompat.postWebMessage do wysyłania ładunku do głównej ramki strony internetowej. Aby utworzyć dwukierunkowy kanał komunikacji, możesz utworzyć WebMessageChannel i przekazać jeden z jego portów wraz z wiadomością do treści internetowych.

Cechy:

  • Asynchroniczna: podobnie jak addWebMessageListener ta metoda wykorzystuje asynchroniczne przesyłanie wiadomości, które zapewnia, że strona internetowa pozostaje responsywna na interakcje użytkownika, podczas gdy aplikacja przetwarza dane w tle.
  • Świadomość pochodzenia: możesz określić targetOrigin, aby mieć pewność, że WebView dostarcza dane tylko do zaufanej witryny.

Ograniczenia:

  • Zakres: ten interfejs API ogranicza komunikację do głównej ramki. Nie obsługuje bezpośredniego adresowania ani wysyłania wiadomości do elementów iframe.
  • Ograniczenia dotyczące identyfikatorów URI: nie możesz używać tej metody w przypadku treści wczytywanych za pomocą identyfikatorów URI data:, file: lub loadData(), chyba że jako docelowe źródło podasz „*”. Dzięki temu każda strona może otrzymać wiadomość.
  • Ryzyko związane z tożsamością: nie ma jasnego sposobu na zweryfikowanie tożsamości nadawcy przez treści internetowe. Wiadomość, którą otrzymuje strona internetowa, może pochodzić z aplikacji natywnej lub z innego elementu iframe.

Użyj tej metody, gdy potrzebujesz prostego, asynchronicznego kanału do przesyłania danych w postaci ciągów znaków we wcześniejszych wersjach Androida, które nie obsługują addWebMessageListener.

Użyj addJavascriptInterface (starsza wersja)

Najstarsza metoda polega na wstrzykiwaniu instancji obiektu natywnego bezpośrednio do elementu WebView.

Jak to działa: definiujesz klasę Kotlin lub Java, dodajesz do dozwolonych metod adnotację @JavascriptInterface i dodajesz instancję klasy do WebView za pomocą addJavascriptInterface(Object, String).

Cechy:

  • Synchroniczne: środowisko wykonawcze JavaScriptu jest blokowane do momentu, gdy metoda w kodzie Androida zwróci wartość.
  • Bezpieczeństwo wątków: system wywołuje metody w wątku w tle, co wymaga starannej synchronizacji po stronie Kotlina lub Javy.
  • Ryzyko związane z bezpieczeństwem: domyślnie interfejs addJavascriptInterface jest dostępny dla każdej ramki w komponencie WebView, w tym dla ramek iframe. Nie ma kontroli dostępu opartej na pochodzeniu. Ze względu na asynchroniczne działanie komponentu WebView nie można bezpiecznie określić adresu URL ramki, która wywołuje Twój interfejs. Nie możesz polegać na metodach takich jak WebView.getUrl() w przypadku weryfikacji bezpieczeństwa, ponieważ nie gwarantują one dokładności i nie wskazują, która konkretna ramka wysłała żądanie.

Konwersje i przekształcenia typów danych

Podczas korzystania z addJavascriptInterface most Java oparty na Chromium konwertuje typy danych między środowiskiem wykonawczym JavaScriptu a kodem aplikacji na Androida.

W przypadku parametrów metody i wartości zwracanych obowiązują te reguły wymuszania typu.

Mapowanie typów parametrów (JavaScript na Java)

Gdy JavaScript przekazuje argumenty do metody Java lub Kotlin z adnotacjami, most wymusza przekształcenie wartości JavaScript na odpowiednie typy parametrów Java:

Typ parametru Java Wartość argumentu JavaScript Zachowania wymuszające
byte, short, int, long Liczba (całkowita) Wartości są rzutowane na docelowy typ liczby całkowitej. Wartości spoza zakresu są zawijane zgodnie ze standardowymi regułami rzutowania liczbowego.
byte, short, int, long NaN Wymusza wartość 0.
byte, short, int, long Infinity Wymusza wartość -1 w przypadku byteshort lub Integer.MAX_VALUELong.MAX_VALUE w przypadku intlong.
float, double Liczba Wymusza konwersję na odpowiednią reprezentację zmiennoprzecinkową w języku Java.
float, double NaN/Infinity Wymusza wartość Float.NaN, Double.NaN, Float.POSITIVE_INFINITY lub Double.POSITIVE_INFINITY.
char Liczba (całkowita) został przekonwertowany na odpowiedni punkt kodowy Unicode.
char Liczba niecałkowita, NaN, Infinity Wymusza wartość \u0000.
boolean true/false Wymusza konwersję na typ Java true lub false.
boolean Liczba, ciąg znaków, obiekt Wymusza wartość false (w tym niepuste ciągi znaków i liczby różne od zera).
String Ciąg znaków Wartość ciągu tekstowego zostanie zachowana.
String Liczba, wartość logiczna Sformatowane jako reprezentacja ciągu znaków (np. "42", "true", "false").
String null/undefined null jest przekształcane na Java null; undefined jest przekształcane na ciąg znaków "undefined".
String Object, ArrayBuffer, TypedArray Wymusza przekształcenie na ciąg znaków "undefined".
Tablica typu prostego (np. int[], byte[], boolean[]) lub String[] Tablica ([...]) Konwertuje na jednowymiarową tablicę Java typu elementu docelowego. Tablice rzadkie wypełniają nieprzypisane indeksy wartościami domyślnymi (0, false, null).
Tablica typów prostych (np. int[], byte[]) TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) Elementy są przekształcane w odpowiednią tablicę typów prostych Java.
tablicę wielowymiarową (np. int[][]), Zagnieżdżona tablica ([[...]]) Nieobsługiwane. Parametry tablic wielowymiarowych mają wartość null.
ArrayBuffer, DataView ArrayBuffer, DataView Nieobsługiwane jako tablice. Instancje ArrayBuffer i DataView są oceniane jako null.
Object lub klasa niestandardowa Obiekt JavaScript ({...}) Nieobsługiwane. Literały obiektów JavaScript o dowolnej strukturze są w języku Java wartościowane jako null.
Object lub klasa niestandardowa Kontener wstrzykniętego obiektu Java Obsługiwane (w obie strony). Przekazuje bazową instancję Javy do metody Javy. Zgłasza wyjątek JavaScript, jeśli typ Java nie pasuje do podpisu parametru.
Typy w ramkach (np. Integer, Double, Boolean) Liczba, wartość logiczna Nieobsługiwane. Typy proste w postaci opakowanej są traktowane jako nieprzejrzyste obiekty i zwracają wartość null.
Dowolny typ prosty null/undefined Wymusza wartości domyślne (0, 0.0, \u0000, false).
Object, String, tablica null Wymusza konwersję na typ Java null.
Mapowanie typów zwracanych (Java na JavaScript)

Gdy metoda w języku Java lub Kotlin z adnotacjami zwraca wartość, interfejs przekształca ją w typ JavaScript:

Typ zwracany w Javie Wartość JavaScript JavaScript typeof
boolean true/false "boolean"
byte, short, int, long, float, double Liczba "number"
char Liczba (punkt kodowy Unicode) "number"
String (niepusty) Wartość łańcuchowa "string"
String (null) undefined "undefined"
void undefined "undefined"
Tablica Java (np. int[], String[]) undefined "undefined". Wartości zwracane w postaci tablicy nie są obsługiwane. Metoda Java nie jest wykonywana, a wartość undefined jest zwracana bez zgłaszania wyjątku.
Obiekt Java / typ niestandardowy (nie może mieć wartości null) Kod obiektu "object". Tworzy otokę JavaScript wokół instancji Javy. Kod JavaScript może wywoływać dowolną publiczną metodę tego obiektu, która jest oznaczona adnotacją @JavascriptInterface.
Obiekt Java / typ niestandardowy (null) null "object"
Typ prosty w pudełku (np. Integer, Double) Kod obiektu "object". Zwracana jako nieprzezroczysty obiekt Java z opakowaniem bez dostępnych metod @JavascriptInterface, co sprawia, że wartość jest bezużyteczna w JavaScript.

Dostępność metody i elementu

Most JavaScript wymusza ścisłe reguły dostępu do elementów i widoczności, aby chronić przed niezamierzonym wykonaniem kodu:

  • Pola nie są udostępniane: pola Javy (w tym pola publicpublic final) nie są dostępne z JavaScriptu i mają wartość undefined.
  • Wymagania dotyczące adnotacji: w JavaScript są dostępne tylko metody, które mają adnotację @JavascriptInterface.
  • Ograniczenia widoczności: metody muszą być public. Metody privateprotected nigdy nie są udostępniane JavaScriptowi, nawet jeśli mają adnotację @JavascriptInterface.
  • Metody statyczne: metody statyczne oznaczone adnotacją @JavascriptInterface można wywoływać z JavaScriptu.
  • Dziedziczenie i zastępowanie: adnotacje @JavascriptInterface nie są dziedziczone, gdy podklasa zastępuje metodę. Jeśli podklasa zastępuje metodę z adnotacją z klasy nadrzędnej, podklasa musi jawnie zawierać adnotację @JavascriptInterface w zastąpionej metodzie, aby udostępnić ją JavaScriptowi. Nieprzesłonięte metody publiczne odziedziczone po klasie nadrzędnej pozostają dostępne, jeśli są oznaczone w klasie nadrzędnej.
  • Ochrona przed odbiciem: standardowe metody odbicia w języku Java (np.getClass()) są blokowane i wywołują wyjątek JavaScriptu, aby zapobiegać lukom w zabezpieczeniach umożliwiającym zdalne wykonywanie kodu.
  • Przeciążanie metod: przeciążone metody w języku Java są obsługiwane. Metoda bridge resolves wywołuje metody na podstawie liczby przekazanych argumentów i nie uwzględnia typów argumentów. Wywołanie przeciążonej metody z nieprawidłową liczbą argumentów powoduje zgłoszenie wyjątku JavaScript. Jeśli 2 przeciążenia mają taką samą liczbę argumentów, jedno z nich zostanie wybrane w sposób losowy.

Podsumowanie mechanizmów

Tabela poniżej zawiera szybkie porównanie 3 głównych mechanizmów implementacji natywnego pomostu:

Metoda addWebMessageListener postWebMessage addJavascriptInterface
Implementacja Asynchroniczne (detektor w wątku głównym) Asynchroniczny Synchroniczna
Bezpieczeństwo Najwyższe (na podstawie listy dozwolonych) Wysoka (z uwzględnieniem źródła) Niski (bez sprawdzania pochodzenia)
złożoność Umiarkowana Umiarkowana Prosty
Kierunek Dwukierunkowe Dwukierunkowe Przeglądarka – aplikacja
Minimalna wersja WebView Wersja 82 (i Jetpack Webkit 1.3.0) Wersja 45 (i Jetpack Webkit 1.1.0) Wszystkie wersje
Zalecane Tak Nie Nie

Obsługa dużych transferów danych

Podczas przesyłania dużych pakietów danych, takich jak ciągi znaków o rozmiarze wielu megabajtów lub pliki binarne, musisz starannie zarządzać pamięcią, aby uniknąć błędów ANR („Aplikacja nie odpowiada”) lub awarii na urządzeniach 32-bitowych. W tej sekcji omówiono różne techniki i ograniczenia związane z przesyłaniem dużych ilości danych między aplikacją hosta a treściami internetowymi.

Przesyłanie danych binarnych za pomocą tablic bajtów

Za pomocą klasy WebMessageCompat możesz wysyłać tablice byte[] bezpośrednio, zamiast serializować dane binarne do ciągów Base64. Kodowanie Base64 zwiększa rozmiar danych o około 33%, więc to rozwiązanie jest znacznie bardziej wydajne pod względem pamięci i szybsze.

  • Przewaga danych binarnych: przesyłaj dane binarne, takie jak pliki obrazów lub audio, między aplikacją natywną a treściami internetowymi.
  • Ograniczenie: nawet w przypadku tablic bajtów system kopiuje dane przez granicę komunikacji międzyprocesowej (IPC) między aplikacją a izolowanym procesem, którego WebView używa do renderowania treści internetowych. W przypadku bardzo dużych plików nadal zużywa to znaczną ilość pamięci.

Poniższe przykłady kodu pokazują, jak skonfigurować addWebMessageListener w aplikacji natywnej, aby odbierać wiadomości oznaczone symbolem WebMessageCompat.TYPE_ARRAY_BUFFER i opcjonalnie odpowiadać danymi binarnymi, sprawdzając 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
      );
  }
}

Poniższy kod JavaScript pokazuje implementację po stronie klienta interfejsu addWebMessageListener, która umożliwia treściom internetowym wysyłanie i odbieranie danych binarnych (ArrayBuffer) do i z aplikacji natywnej za pomocą wstrzykniętego w poprzednim przykładzie serwera proxy window.myBridge.

// 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]);
        }
    };
}

Wydajne wczytywanie danych na dużą skalę

W przypadku bardzo dużych plików (powyżej 10 MB) użyj metody shouldInterceptRequest do przesyłania strumieniowego danych:

  1. Strona internetowa inicjuje fetch() wywołanie niestandardowego adresu URL zastępczego. Na przykład: https://app.local/large-file.
  2. Aplikacja na Androida przechwytuje to żądanie w funkcji WebViewClient.shouldInterceptRequest.
  3. Aplikacja zwraca dane w postaci InputStream.

Umożliwia to przesyłanie strumieniowe danych w częściach zamiast wczytywania całego ładunku do pamięci naraz.

Poniższa funkcja JavaScriptu pokazuje kod po stronie klienta, który umożliwia wydajne wczytywanie dużego pliku binarnego z aplikacji natywnej za pomocą standardowego wywołania fetch() niestandardowego adresu URL zastępczego.

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);
    }
}

Poniższe przykłady kodu pokazują, jak po stronie aplikacji natywnej za pomocą metody WebViewClient.shouldInterceptRequest w językach Kotlin i Java przesyłać strumieniowo duży plik binarny, przechwytując niestandardowy adres URL symbolu zastępczego, o który poprosiła zawartość internetowa.

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);
  }
});

Postępuj zgodnie z zaleceniami dotyczącymi bezpieczeństwa

Aby chronić aplikację i dane użytkowników, podczas wdrażania pomostu postępuj zgodnie z tymi wytycznymi:

  • Wymuszanie protokołu HTTPS: aby mieć pewność, że złośliwe treści osób trzecich nie mogą wywoływać natywnej logiki aplikacji, zezwalaj tylko na komunikację z bezpiecznymi źródłami.

  • Korzystaj z reguł pochodzenia: najlepszym sposobem na radzenie sobie z zaufaniem jest ścisłe zdefiniowanie allowedOriginRules i sprawdzenie sourceOrigin podanego w wywołaniu zwrotnym wiadomości. Unikaj używania pełnego symbolu wieloznacznego (*), który pasuje do wszystkich źródeł, jako jedynej reguły dotyczącej źródeł, chyba że jest to absolutnie konieczne. Używanie symboli wieloznacznych w przypadku subdomen (np. *.example.com) pozostaje prawidłowe i bezpieczne w przypadku dopasowywania wielu subdomen (np. foo.example.com, bar.example.com).

    Uwaga: reguły dotyczące pochodzenia chronią przed złośliwymi witrynami innych firm i ukrytymi elementami iframe, ale nie chronią przed lukami w zabezpieczeniach typu cross-site scripting (XSS) w Twojej własnej zaufanej domenie. Jeśli na przykład Twoja strona internetowa wyświetla treści użytkowników i jest podatna na ataki typu stored XSS, osoba przeprowadzająca atak może wykonać skrypt działający jako zaufane źródło. Rozważ zastosowanie weryfikacji w przypadku ładunków wiadomości przed wykonaniem operacji na platformie natywnej o znaczeniu krytycznym.

  • Zminimalizuj obszar: udostępniaj tylko konkretne metody lub dane, których wymaga strona internetowa.

  • Sprawdzanie funkcji w czasie działania: najnowsze interfejsy API pomostowe, w tym addWebMessageListener, są częścią biblioteki Jetpack Webkit. Zanim zadzwonisz do zespołu pomocy, zawsze sprawdź, czy możesz skorzystać z WebViewFeature.isFeatureSupported().