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 alanları

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ı aktarılması.

İ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 vardır.

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 sayfaya gitmeden önce addWebMessageListener işlevini çağırmanız gerekir (ör. WebViewCompat.navigate veya loadUrl işlevini çağırma).

Temel özellikler:

  • Güvenlik ve güven: Bu yöntem, eski API'lerin aksine başlatma 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 bu kaynaktan yüklenen web sayfalarına sunacağını garanti eder.

    Yerel dinleyici geri çağırma işlevi, her iletiyle 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 yükün titiz bir şekilde doğrulanması gerekmez.

    • WebView, şemaya (HTTP/HTTPS), ana makineye ve bağlantı noktasına göre kuralları 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 karakterler 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 (UI) 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 yük 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, sayfaya istediğiniz zaman 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 sayfa gezinme işlemini başlatır (ör. WebViewCompat.navigate veya loadUrl ile).
  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ı dinleyici 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.
  • Kaynağa duyarlı: 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 ileti, 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) kullanma

En eski yöntemde, yerel bir nesne örneği doğrudan WebView'a yerleştirilir.

İşleyiş şekli: Bir Kotlin veya Java sınıfı tanımlar, izin verilen yöntemleri @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, yöntemleri arka plan iş parçacığında ç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. WebView.getUrl() gibi yöntemler, doğru oldukları garanti edilmediği ve isteği hangi belirli çerçevenin yaptığı belirtilmediği için güvenlik doğrulaması amacıyla kullanılmamalıdır.

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 Senkronize
Güvenlik En yüksek (izin verilenler listesine dayalı) Yüksek (kaynak bilgisiyle) Düşük (Kaynak kontrolü yok)
Karmaşıklık Orta Orta Basit
Yön İki yönlü İki 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, uygulama ile WebView'ın web içeriğini oluşturmak için kullandığı yalıtılmış işlem arasındaki işlemler arası iletişim (IPC) sınırı boyunca verileri kopyalar. Bu yöntem, çok büyük dosyalar için önemli miktarda bellek kullanmaya devam eder.

Aşağıdaki kod örneklerinde, WebMessageCompat.TYPE_ARRAY_BUFFER işaretli 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.

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 öğesinin istemci tarafı 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 sayede, tüm yükü aynı anda belleğe yüklemek yerine veriler parçalar halinde yayınlanabilir.

Aşağıdaki JavaScript işlevi, yerel uygulamadan büyük bir ikili program dosyasını standart bir fetch() özel yer tutucu URL'ye yapılan çağrı kullanarak verimli bir şekilde yüklemek 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 uyma

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 bir 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 uygulayabilirsiniz.

  • 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, ilgili numarayı aramadan önce WebViewFeature.isFeatureSupported() kullanarak destek alıp alamayacağınızı kontrol edin.