Questa pagina illustra i vari metodi e le best practice per stabilire un bridge nativo, noto anche come bridge JavaScript, per facilitare la comunicazione tra i contenuti web in un WebView e un'app per Android host.
In questo modo, gli sviluppatori web possono utilizzare JavaScript per accedere alle funzionalità della piattaforma nativa, come la fotocamera, il file system o i sensori hardware avanzati, che le API web standard non forniscono normalmente.
Casi d'uso
Un'implementazione del bridge JavaScript consente vari scenari di integrazione in cui i contenuti web richiedono un accesso più approfondito al sistema operativo Android. Di seguito sono riportati alcuni esempi:
- Integrazione della piattaforma: attivazione dei componenti dell'interfaccia utente nativa di Android (ad
esempio, prompt biometrici,
BottomSheetDialog) da una pagina web. - Prestazioni: offload di attività di calcolo complesse al codice Java o Kotlin nativo.
- Persistenza dei dati: accesso a database criptati locali o preferenze condivise.
- Trasferimenti di dati di grandi dimensioni: passaggio di file multimediali o strutture di dati complesse tra l'app e il renderer web.
Meccanismi di comunicazione
Android offre tre generazioni principali di API per stabilire un bridge nativo. Sebbene siano tutte ancora disponibili, differiscono in modo significativo in termini di sicurezza, usabilità e prestazioni.
Utilizza addWebMessageListener (opzione consigliata)
addWebMessageListener è l'approccio più moderno e consigliato per la comunicazione tra i contenuti web e il codice dell'app nativa. Combina la facilità d'uso dell'interfaccia JavaScript con la sicurezza del sistema di messaggistica.
Come funziona: l'app aggiunge un listener con un nome specifico e un insieme di
regole di origine consentite. WebView garantisce quindi che l'oggetto JavaScript sia presente nell'ambito globale (window.objectName) dal momento in cui la pagina inizia a caricarsi.
Inizializzazione: per assicurarti che WebView inserisca l'oggetto JavaScript prima dell'esecuzione di
qualsiasi script, devi chiamare addWebMessageListener prima di passare alla
pagina (ad esempio chiamando WebViewCompat.navigate o loadUrl).
Funzionalità principali:
Sicurezza e attendibilità: a differenza delle API legacy, questo metodo richiede un
Set<String>diallowedOriginRulesdurante l'inizializzazione. Questo è il meccanismo principale per stabilire l'attendibilità.Quando specifichi un'origine attendibile, ad esempio
https://example.com, WebView garantisce che esponga gli oggetti JavaScript inseriti solo alle pagine web caricate da quell'origine esatta.Il callback del listener nativo riceve un parametro
sourceOrigincon ogni messaggio. Puoi utilizzarlo per verificare l'origine esatta del mittente se il tuo bridge supporta più origini consentite.Poiché WebView applica rigorosamente questi controlli di origine a livello di piattaforma, in genere la tua app può considerare veritieri i messaggi ricevuti da un
sourceOriginattendibile, eliminando la necessità di una rigorosa convalida del payload nella maggior parte delle implementazioni standard.- WebView confronta le regole con lo schema (HTTP/HTTPS), l'host e la porta.
- WebView ignora i percorsi. Ad esempio,
https://example.comconsentehttps://example.com/loginehttps://example.com/home. - WebView limita rigorosamente i caratteri jolly all'inizio dell'host per i sottodomini. Ad esempio,
https://*.example.comcorrisponde ahttps://foo.example.com, ma non ahttps://example.com. Se devi trovare la corrispondenza sia conhttps://example.comsia con i relativi sottodomini, devi aggiungere ogni regola di origine separatamente alla lista consentita (ad esempio,"https://example.com", "https://*.example.com"). Non puoi utilizzare caratteri jolly per lo schema o al centro di un dominio.
In questo modo, il bridge è limitato ai domini verificati, impedendo l'esecuzione di codice nativo da parte di contenuti di terze parti non autorizzati o iframe inseriti.
Supporto multi-frame: funziona su tutti i frame che corrispondono alle regole di origine
Threading: il callback del listener viene eseguito nel thread principale (UI) dell'applicazione. Se il tuo bridge deve gestire l'elaborazione di dati complessi, l'analisi JSON o le ricerche di database, devi trasferire questo lavoro a un thread in background per evitare di bloccare l'interfaccia utente dell'applicazione con un errore "L'app non risponde" (ANR).
Bidirezionale: quando la pagina web invia un messaggio, l'app riceve un
JavaScriptReplyProxyche può utilizzare per inviare messaggi a quel frame specifico. Puoi conservare questo oggettoreplyProxye utilizzarlo in qualsiasi momento per inviare un numero qualsiasi di messaggi alla pagina, non solo per rispondere a ogni singolo messaggio inviato dalla pagina. Se il frame di origine esce dalla navigazione o viene eliminato, i messaggi inviati utilizzandopostMessage()sul proxy vengono ignorati silenziosamente.Avvio lato app: sebbene la pagina web debba sempre avviare il canale di comunicazione con l'app, l'app nativa può richiedere unilateralmente alla pagina web di avviare questo processo. L'app nativa può comunicare con la pagina web utilizzando
addDocumentStartJavaScript()(per valutare JavaScript prima del caricamento della pagina) oevaluateJavaScript()(per valutare JavaScript dopo il caricamento della pagina).
Limitazione: questa API invia i dati come stringhe o byte[] array. Per strutture di dati più complesse, ad esempio oggetti JSON, devi serializzarle in uno di questi formati e poi deserializzarle dall'altra parte per ricostruire la struttura dei dati.
Esempio di utilizzo:
Per comprendere la sequenza completa di uno scambio di messaggi bidirezionale, gli eventi procedono nel seguente ordine:
- Avvio (app): l'app nativa registra il listener con
addWebMessageListenere avvia la navigazione della pagina (ad esempio conWebViewCompat.navigateoloadUrl). - Invio del messaggio (web): il codice JavaScript della pagina web chiama
myObject.postMessage(message)per avviare la comunicazione. - Ricezione e risposta del messaggio (app): l'app riceve il messaggio nel
callback del listener e risponde utilizzando il
replyProxy.postMessage()fornito. - Ricezione della risposta (web): la pagina web riceve la risposta asincrona nella
myObject.onmessage()funzione di callback.
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);
}
Il seguente codice JavaScript mostra l'implementazione lato client di addWebMessageListener, che consente ai contenuti web di ricevere messaggi dall'app nativa e di inviare i propri messaggi tramite il proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Utilizza postWebMessage (alternativa)
Android ha introdotto questa alternativa asincrona basata sulla messaggistica simile a window.postMessage del web.
Come funziona: l'app utilizza WebViewCompat.postWebMessage per inviare un payload
al frame principale della pagina web. Per stabilire un canale di comunicazione
bidirezionale, puoi creare un WebMessageChannel e passare una delle sue
porte con il messaggio ai contenuti web.
Caratteristiche:
- Asincrono: come
addWebMessageListener, questo metodo utilizza la messaggistica asincrona, che garantisce che la pagina web rimanga reattiva alle interazioni dell'utente mentre l'app elabora i dati in background. - Rilevamento dell'origine: puoi specificare un
targetOriginper assicurarti che WebView fornisca i dati solo a un sito web attendibile.
Limitazioni:
- Ambito: questa API limita la comunicazione al frame principale. Non supporta l'indirizzamento diretto o l'invio di messaggi agli iframe.
- Limitazioni URI: non puoi utilizzare questo metodo per i contenuti caricati utilizzando gli URI
data:, gli URIfile:oloadData(), a meno che non specifichi "*" come origine di destinazione. In questo modo, qualsiasi pagina può ricevere il messaggio. - Rischio di identità: non esiste un modo chiaro per i contenuti web di verificare l' identità del mittente. Un messaggio ricevuto dalla pagina web potrebbe provenire dalla tua app nativa o da un altro iframe.
Utilizza questo metodo quando hai bisogno di un canale asincrono semplice per i dati basati su stringhe nelle versioni precedenti di Android che non supportano addWebMessageListener.
Utilizza addJavascriptInterface (legacy)
Il metodo più vecchio prevede l'inserimento diretto di un'istanza di oggetto nativo in WebView.
Come funziona: definisci una classe Kotlin o Java, annota i metodi consentiti
con @JavascriptInterface e aggiungi un'istanza della classe a
WebView utilizzando addJavascriptInterface(Object, String).
Caratteristiche:
- Sincrono: l'ambiente di esecuzione JavaScript si blocca finché il metodo nel codice Android non restituisce un valore.
- Thread safety: il sistema chiama i metodi su un thread in background, richiedendo una sincronizzazione accurata sul lato Kotlin o Java.
- Rischio per la sicurezza: per impostazione predefinita,
addJavascriptInterfaceè disponibile per ogni frame all'interno di WebView, inclusi gli iframe. Non dispone di un controllo dell'accesso basato sull'origine. A causa del comportamento asincrono di WebView, non è possibile determinare in modo sicuro l'URL del frame che chiama l'interfaccia. Non devi fare affidamento su metodi comeWebView.getUrl()per la verifica della sicurezza, in quanto non è garantita la loro accuratezza e non indicano quale frame specifico ha effettuato la richiesta.
Riepilogo dei meccanismi
La seguente tabella fornisce un confronto rapido dei tre meccanismi principali di implementazione del bridge nativo:
| Metodo | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implementazione | Asincrona (listener sul thread principale) | Asincrona | Sincrona |
| Sicurezza | Massima (basata su lista consentita) | Elevata (rilevamento dell'origine) | Bassa (nessun controllo dell'origine) |
| Complessità | Moderata | Moderata | Semplice |
| Direzione | Bidirezionale | Bidirezionale | Da web ad app |
| Versione minima di WebView | Versione 82 (e Jetpack Webkit 1.3.0) | Versione 45 (e Jetpack Webkit 1.1.0) | Tutte le versioni |
| Consigliato | Sì | No | No |
Gestire i trasferimenti di dati di grandi dimensioni
Quando trasferisci payload di grandi dimensioni, come stringhe o file binari di più megabyte, devi gestire attentamente la memoria per evitare errori "L'applicazione non risponde" (ANR) o arresti anomali sui dispositivi a 32 bit. Questa sezione illustra le varie tecniche e limitazioni associate al trasferimento di quantità significative di dati tra l'applicazione host e i contenuti web.
Trasferire dati binari con array di byte
Con la classe WebMessageCompat, puoi inviare direttamente array byte[]
anziché serializzare i dati binari in stringhe Base64. Poiché Base64 aggiunge un overhead di circa il 33% alle dimensioni dei dati, questo è significativamente più efficiente in termini di memoria e più veloce.
- Vantaggio binario: trasferisci dati binari come file immagine o audio tra l'app nativa e i contenuti web.
- Limitazione: anche con gli array di byte, il sistema copia i dati attraverso il limite di comunicazione tra processi (IPC) tra l'app e il processo isolato utilizzato da WebView per il rendering dei contenuti web. Questo consuma comunque una quantità significativa di memoria per i file di dimensioni molto grandi.
I seguenti esempi di codice mostrano come configurare addWebMessageListener sul lato dell'app nativa per ricevere i messaggi contrassegnati con WebMessageCompat.TYPE_ARRAY_BUFFER e, facoltativamente, rispondere con dati binari controllando 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
);
}
}
Il seguente codice JavaScript mostra l'implementazione lato client di addWebMessageListener, che consente ai contenuti web di inviare e ricevere dati binari (ArrayBuffer) all'app nativa e dall'app nativa utilizzando il proxy window.myBridge inserito nell'esempio precedente.
// 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]);
}
};
}
Caricamento efficiente di dati su larga scala
Per file di dimensioni molto grandi (> 10 MB), utilizza il metodo shouldInterceptRequest per lo
streaming dei dati:
- La pagina web avvia una chiamata
fetch()a un URL personalizzato con segnaposto. Ad esempio,https://app.local/large-file. - L'app per Android intercetta questa richiesta in
WebViewClient.shouldInterceptRequest. - L'app restituisce i dati come
InputStream.
In questo modo, i dati vengono trasmessi in streaming in blocchi anziché caricare l'intero payload in memoria contemporaneamente.
La seguente funzione JavaScript mostra il codice lato client per caricare in modo efficiente un file binario di grandi dimensioni dall'applicazione nativa utilizzando una chiamata fetch() standard a un URL personalizzato con segnaposto.
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);
}
}
I seguenti esempi di codice mostrano il lato dell'app nativa, utilizzando il metodo WebViewClient.shouldInterceptRequest in Kotlin e Java, per trasmettere in streaming un file binario di grandi dimensioni intercettando un URL personalizzato con segnaposto richiesto dai contenuti web.
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);
}
});
Segui i consigli per la sicurezza
Per proteggere l'applicazione e i dati utente, segui queste linee guida quando implementi un bridge:
Applica HTTPS: per assicurarti che i contenuti di terze parti dannosi non possano richiamare la logica nativa della tua applicazione, consenti la comunicazione solo con origini sicure.
Affidati alle regole di origine: il modo migliore per gestire l'attendibilità è definire rigorosamente le
allowedOriginRulese controllare lasourceOriginfornita nel callback del messaggio. Evita di utilizzare il carattere jolly completo (*), che corrisponde a tutte le origini, come unica regola di origine, a meno che non sia assolutamente necessario. L'utilizzo di caratteri jolly per i sottodomini (ad esempio,*.example.com) rimane valido e sicuro per la corrispondenza con più sottodomini (ad esempio,foo.example.com,bar.example.com).Nota: sebbene le regole di origine proteggano da siti web di terze parti dannosi e iframe nascosti, non possono proteggere dalle vulnerabilità di cross-site scripting (XSS) all'interno del tuo dominio attendibile. Ad esempio, se la tua pagina web mostra contenuti generati dagli utenti ed è vulnerabile a XSS memorizzato, un utente malintenzionato potrebbe eseguire uno script che funge da origine attendibile. Valuta la possibilità di applicare la convalida ai payload dei messaggi prima di eseguire operazioni sensibili della piattaforma nativa.
Riduci al minimo la superficie di attacco: esponi solo i metodi o i dati specifici richiesti dalla pagina web.
Controlla le funzionalità in fase di runtime: le API bridge recenti, inclusa
addWebMessageListener, fanno parte della libreria Jetpack Webkit. Pertanto, controlla sempre il supporto utilizzandoWebViewFeature.isFeatureSupported()prima di chiamarle.