JavaScript köprüsüyle yerel API'lere erişme

Bu sayfada, WebView içindeki web içeriği ile ana Android uygulaması arasındaki iletişimi kolaylaştırmak için JavaScript köprüsü olarak da bilinen yerel köprü oluşturmaya yönelik çeşitli yöntemler ve en iyi uygulamalar ele alınmaktadır.

Bu sayede web geliştiriciler, standart web API'lerinin normalde sağlamadığı yerel platform özelliklerine (ör. kamera, dosya sistemi veya gelişmiş donanım sensörleri) erişmek için JavaScript'i kullanabilir.

Kullanım örnekleri

JavaScript köprüsü uygulaması, web içeriğinin Android işletim sistemine daha derin erişim gerektirdiği çeşitli entegrasyon senaryolarına olanak tanır. Aşağıda bazı örnekler verilmiştir:

  • Platform entegrasyonu: Bir web sayfasından yerel Android kullanıcı arayüzü bileşenlerini (ör. biyometri istemleri, BottomSheetDialog) tetikleme.
  • Performans: Ağır hesaplama görevlerini yerel Java veya Kotlin koduna aktarma.
  • Veri kalıcılığı: Yerel şifrelenmiş veritabanlarına veya paylaşılan tercihlere erişme.
  • Büyük veri aktarımları: Uygulama ile web oluşturucu arasında medya dosyaları veya karmaşık veri yapıları aktarma.

İletişim mekanizmaları

Android, yerel bir köprü oluşturmak için üç temel API nesli sunar. Bu uygulamaların tümü hâlâ kullanılabilir olsa da güvenlik, kullanılabilirlik ve performans açısından önemli farklılıklar gösterir.

addWebMessageListener kullanın (önerilir)

addWebMessageListener, web içeriği ile yerel uygulama kodu arasındaki iletişim için en modern ve önerilen yaklaşımdır. JavaScript arayüzünün kullanım kolaylığı ile mesajlaşma sisteminin güvenliğini birleştirir.

İşleyiş şekli: Uygulama, belirli bir ada ve bir dizi izin verilen kaynak kuralına sahip bir dinleyici ekler. Ardından WebView, sayfa yüklenmeye başladığı andan itibaren JavaScript nesnesinin genel kapsamda (window.objectName) bulunduğundan emin olur.

Başlatma: WebView'ın JavaScript nesnesini herhangi bir komut dosyası çalıştırılmadan önce eklemesini sağlamak için loadUrl() çağrısından önce addWebMessageListener çağrısı yapmanız gerekir.

Temel özellikler:

  • Güvenlik ve güven: Eski API'lerden farklı olarak bu yöntem, ilk kullanıma hazırlama sırasında Set<String> allowedOriginRules gerektirir. Bu, güven oluşturmanın temel mekanizmasıdır.

    https://example.com gibi güvenilir bir kaynak belirttiğinizde WebView, yerleştirilen JavaScript nesnelerini yalnızca tam olarak bu kaynaktan yüklenen web sayfalarına sunacağını garanti eder.

    Yerel dinleyici geri çağırma işlevi, her mesajla birlikte bir sourceOrigin parametresi alır. Köprünüz birden fazla izin verilen kaynağı destekliyorsa gönderenin tam kaynağını doğrulamak için bu özelliği kullanabilirsiniz.

    WebView, bu kaynak kontrollerini platform düzeyinde sıkı bir şekilde uyguladığından uygulamanız genellikle güvenilir bir sourceOrigin kaynağından alınan mesajların doğru olduğuna güvenebilir. Bu sayede, çoğu standart uygulamada sıkı bir yük doğrulama işlemine gerek kalmaz.

    • WebView, kuralları şemaya (HTTP/HTTPS), ana makineye ve bağlantı noktasına göre eşleştirir.
    • WebView, yolları yoksayar. Örneğin, https://example.com, https://example.com/login ve https://example.com/home'ye izin veriyor.
    • WebView, alt alan adları için joker karakterleri ana makinenin başlangıcıyla kesin olarak sınırlar. Örneğin, https://*.example.com, https://foo.example.com ile eşleşir ancak https://example.com ile eşleşmez. Hem https://example.com hem de alt alan adlarını eşleştirmeniz gerekiyorsa her kaynak kuralını izin verilenler listesine ayrı ayrı eklemeniz gerekir (örneğin, "https://example.com", "https://*.example.com"). Şema için veya bir alanın ortasında joker karakter kullanamazsınız.

    Bu, köprüyü doğrulanmış alanlarla kısıtlayarak yetkisiz üçüncü taraf içeriğinin veya yerleştirilmiş iframe'lerin yerel kodu yürütmesini engeller.

  • Çoklu çerçeve desteği: Kaynak kurallarıyla eşleşen tüm çerçevelerde çalışır.

  • İş parçacığı oluşturma: Dinleyici geri çağırma işlemi, uygulamanın ana (kullanıcı arayüzü) iş parçacığında çalışır. Köprünüzün karmaşık veri işleme, JSON ayrıştırma veya veritabanı aramaları yapması gerekiyorsa "uygulama yanıt vermiyor" (ANR) hatasıyla uygulama kullanıcı arayüzünün donmasını önlemek için bu işi arka plan iş parçacığına aktarmanız gerekir.

  • İki yönlü: Web sayfası mesaj gönderdiğinde uygulama, bu belirli çerçeveye mesaj göndermek için kullanabileceği bir JavaScriptReplyProxy alır. Bu replyProxy nesnesini saklayabilir ve sayfaya gönderilen her iletiye tek tek yanıt vermek yerine istediğiniz zaman sayfaya istediğiniz sayıda ileti göndermek için kullanabilirsiniz. Kaynak çerçeve başka bir yere giderse veya yok edilirse proxy'de postMessage() kullanılarak gönderilen iletiler sessizce yoksayılır.

  • Uygulama tarafında başlatma: Web sayfası her zaman uygulama ile iletişim kanalını başlatmalıdır. Ancak yerel uygulama, web sayfasını bu süreci başlatmaya tek taraflı olarak yönlendirebilir. Yerel uygulama, addDocumentStartJavaScript() (sayfa yüklenmeden önce JavaScript'i değerlendirmek için) veya evaluateJavaScript() (sayfa yüklendikten sonra JavaScript'i değerlendirmek için) ile web sayfasıyla iletişim kurabilir.

Sınırlama: Bu API, verileri dizeler veya byte[] dizileri olarak gönderir. JSON nesneleri gibi daha karmaşık veri yapıları için bu verileri biçimlerden birine seri hale getirmeniz ve ardından veri yapısını yeniden oluşturmak için diğer tarafta seri halini kaldırmanız gerekir.

Kullanım örneği:

İki yönlü mesaj alışverişinin tüm sırasını anlamak için etkinlikler şu sırayla gerçekleşir:

  1. Başlatma (uygulama): Yerel uygulama, dinleyiciyi addWebMessageListener ile kaydeder ve web sayfasını loadUrl() ile yükler.
  2. Mesaj gönderme (web): İletişimi başlatmak için web sayfasının JavaScript çağrıları myObject.postMessage(message) kullanılır.
  3. Mesaj alma ve yanıtlama (uygulama): Uygulama, mesajı listener geri çağırma işlevinde alır ve sağlanan replyProxy.postMessage() ile yanıtlar.
  4. Yanıt alma (web): Web sayfası, eşzamansız yanıtı myObject.onmessage() geri çağırma işlevinde alır.

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

Aşağıdaki JavaScript, addWebMessageListener öğesinin istemci tarafı uygulamasını gösterir. Bu uygulama, web içeriğinin yerel uygulamadan mesaj almasına ve myObject proxy'si üzerinden kendi mesajlarını göndermesine olanak tanır.

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

postWebMessage (Alternatif) kullanın

Android, web'deki window.postMessage'ye benzer şekilde eşzamansız ve mesajlaşmaya dayalı bir alternatif sunmak için bu özelliği kullanıma sundu.

Nasıl çalışır? Uygulama, web sayfasının ana çerçevesine bir yük göndermek için WebViewCompat.postWebMessage kullanır. Çift yönlü bir iletişim kanalı oluşturmak için WebMessageChannel oluşturabilir ve bağlantı noktalarından birini mesajla birlikte web içeriğine iletebilirsiniz.

Özellikler:

  • Eşzamansız: addWebMessageListener gibi bu yöntem de eşzamansız mesajlaşmayı kullanır. Bu sayede, uygulama verileri arka planda işlerken web sayfası kullanıcı etkileşimlerine yanıt vermeye devam eder.
  • Kaynak bilgisi: WebView'ın verileri yalnızca güvenilir bir web sitesine iletmesini sağlamak için targetOrigin belirtebilirsiniz.

Sınırlamalar:

  • Kapsam: Bu API, iletişimi ana çerçeveyle sınırlar. Doğrudan adresleme veya iFrame'lere mesaj gönderme desteklenmez.
  • URI kısıtlamaları: Hedef kaynak olarak "*" belirtmediğiniz sürece bu yöntemi data: URI'leri, file: URI'leri veya loadData() kullanılarak yüklenen içerikler için kullanamazsınız. Bu işlem, herhangi bir sayfanın mesaj almasına olanak tanır.
  • Kimlik riski: Web içeriğinin, gönderenin kimliğini doğrulayabileceği net bir yöntem yoktur. Web sayfasının aldığı bir mesaj, yerel uygulamanızdan veya başka bir iFrame'den gelmiş olabilir.

addWebMessageListener'yı desteklemeyen eski Android sürümlerinde dize tabanlı veriler için basit ve eşzamansız bir kanal gerektiğinde bu yöntemi kullanın.

addJavascriptInterface (Eski) özelliğini kullanma

En eski yöntem, yerel bir nesne örneğini doğrudan WebView'a yerleştirmeyi içerir.

İşleyiş şekli: Bir Kotlin veya Java sınıfı tanımlar, izin verilen yöntemlere @JavascriptInterface ile açıklama ekler ve addJavascriptInterface(Object, String) kullanarak sınıfın bir örneğini WebView'a eklersiniz.

Özellikler:

  • Eşzamanlı: Android kodunuzdaki yöntem döndürülene kadar JavaScript yürütme ortamı engellenir.
  • İş parçacığı güvenliği: Sistem, arka plan iş parçacığında yöntemleri çağırır. Bu nedenle, Kotlin veya Java tarafında dikkatli bir senkronizasyon gerekir.
  • Güvenlik riski: addJavascriptInterface, varsayılan olarak iFrame'ler dahil olmak üzere WebView'daki her çerçeve için kullanılabilir. Kaynak tabanlı erişim denetimi yoktur. WebView'ın eşzamansız davranışı nedeniyle, arayüzünüzü çağıran çerçevenin URL'sini güvenli bir şekilde belirlemek mümkün değildir. Doğruluğu garanti edilmediği ve hangi belirli karenin isteği gönderdiğini belirtmediği için güvenlik doğrulaması amacıyla WebView.getUrl() gibi yöntemleri kullanmamalısınız.

Mekanizmaların özeti

Aşağıdaki tabloda, üç temel yerel köprü uygulama mekanizmasının kısa bir karşılaştırması verilmiştir:

Yöntem addWebMessageListener postWebMessage addJavascriptInterface
Uygulama Eşzamansız (ana iş parçacığında dinleyici) Eşzamansız Eşzamanlı
Güvenlik En yüksek (izin verilenler listesine dayalı) Yüksek (kaynak bilgisiyle) Düşük (Kaynak kontrolü yok)
Karmaşıklık Orta seviye Orta seviye Basit
Yön Çift yönlü Çift yönlü Web'den uygulamaya
Minimum WebView sürümü Sürüm 82 (ve Jetpack Webkit 1.3.0) Sürüm 45 (ve Jetpack Webkit 1.1.0) Tüm sürümler
Önerilen Evet Hayır Hayır

Büyük veri aktarımlarını işleme

32 bit cihazlarda "Uygulama Yanıt Vermiyor" (ANR) hatalarını veya kilitlenmeleri önlemek için çok megabaytlık dizeler ya da ikili dosyalar gibi büyük yükleri aktarırken belleği dikkatli bir şekilde yönetmeniz gerekir. Bu bölümde, ana makine uygulaması ile web içeriği arasında önemli miktarda veri aktarımıyla ilişkili çeşitli teknikler ve sınırlamalar ele alınmaktadır.

Bayt dizileriyle ikili veri aktarma

WebMessageCompat sınıfıyla, ikili verileri Base64 dizelerine serileştirmek yerine doğrudan byte[] dizileri gönderebilirsiniz. Base64, veri boyutuna yaklaşık% 33 ek yük getirdiğinden bu yöntem, bellek açısından çok daha verimli ve daha hızlıdır.

  • İkili avantaj: Resim dosyaları veya ses gibi ikili verileri yerel uygulamanız ile web içeriğiniz arasında aktarın.
  • Sınırlama: Bayt dizileriyle bile sistem, verileri uygulama ile WebView'ın web içeriğini oluşturmak için kullandığı yalıtılmış işlem arasındaki süreçler arası iletişim (IPC) sınırı boyunca kopyalar. Bu yöntem, çok büyük dosyalar için önemli miktarda bellek kullanmaya devam eder.

Aşağıdaki kod örneklerinde, addWebMessageListener ile işaretlenmiş mesajları almak için yerel uygulama tarafında addWebMessageListener'nın nasıl ayarlanacağı ve isteğe bağlı olarak WebViewFeature.MESSAGE_ARRAY_BUFFER kontrol edilerek ikili verilerle nasıl yanıt verileceği gösterilmektedir.WebMessageCompat.TYPE_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
      );
  }
}

Aşağıdaki JavaScript kodu, addWebMessageListener istemci taraflı uygulamasını gösterir. Bu uygulama, web içeriğinin önceki örnekte yer alan window.myBridge proxy'si kullanılarak yerel uygulamaya ve yerel uygulamadan ikili veri (ArrayBuffer) göndermesini ve almasını sağlar.

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

Büyük ölçekli verileri verimli bir şekilde yükleme

Çok büyük dosyalar (>10 MB) için verileri yayınlamak üzere shouldInterceptRequest yöntemini kullanın:

  1. Web sayfası, özel bir yer tutucu URL'ye fetch() çağrısı başlatır. Örneğin, https://app.local/large-file.
  2. Android uygulaması, bu isteğe WebViewClient.shouldInterceptRequest içinde müdahale eder.
  3. Uygulama, verileri InputStream olarak döndürür.

Bu, tüm yükü aynı anda belleğe yüklemek yerine verilerin parçalar halinde aktarılmasını sağlar.

Aşağıdaki JavaScript işlevi, özel bir yer tutucu URL'ye standart bir fetch() çağrı kullanarak yerel uygulamadan büyük bir ikili program dosyasının verimli bir şekilde yüklenmesi için istemci taraflı kodu gösterir.

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

Aşağıdaki kod örnekleri, web içeriği tarafından istenen özel bir yer tutucu URL'yi yakalayarak büyük bir ikili program dosyasını yayınlamak için hem Kotlin hem de Java'da WebViewClient.shouldInterceptRequest yöntemini kullanarak yerel uygulama tarafını gösterir.

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

Güvenlik önerilerine uyun

Uygulamanızı ve kullanıcı verilerinizi korumak için köprü uygularken aşağıdaki yönergelere uyun:

  • HTTPS'yi zorunlu kılın: Kötü amaçlı üçüncü taraf içeriklerinin uygulamanızın yerel mantığını çağıramaması için yalnızca güvenli kaynaklarla iletişime izin verin.

  • Kaynak kurallarına güvenin: Güvenle ilgili sorunları çözmenin en iyi yolu, allowedOriginRules değerinizi net bir şekilde tanımlamak ve mesaj geri aramasında sağlanan sourceOrigin değerini kontrol etmektir. Kesinlikle gerekli olmadığı sürece, tüm kaynaklarla eşleşen tam joker karakteri (*) tek kaynak kuralınız olarak kullanmaktan kaçının. Alt alan adları için joker karakterler (örneğin, *.example.com) kullanmak, birden fazla alt alan adının (örneğin, foo.example.com, bar.example.com) eşleştirilmesi açısından geçerli ve güvenli olmaya devam eder.

    Not: Kaynak kuralları, kötü amaçlı üçüncü taraf web sitelerine ve gizli iFrame'lere karşı koruma sağlasa da kendi güvenilir alanınızdaki siteler arası komut dosyası çalıştırma (XSS) güvenlik açıklarına karşı koruma sağlayamaz. Örneğin, web sayfanızda kullanıcı tarafından oluşturulan içerik gösteriliyorsa ve depolanmış XSS'ye karşı savunmasızsa saldırgan, güvenilir kaynağınız gibi davranan bir komut dosyası yürütebilir. Hassas yerel platform işlemleri yürütülmeden önce mesaj yüklerine doğrulama uygulamanız önerilir.

  • Yüzey alanını en aza indirin: Yalnızca web sayfasının gerektirdiği belirli yöntemleri veya verileri kullanıma sunun.

  • Çalışma zamanında özellikleri kontrol etme: addWebMessageListener dahil olmak üzere son köprü API'leri, Jetpack Webkit kitaplığının bir parçasıdır. Bu nedenle, destek ekibini aramadan önce WebViewFeature.isFeatureSupported() kullanarak destek alıp alamayacağınızı kontrol edin.