Cette page présente les différentes méthodes et bonnes pratiques pour établir un
pont natif, également appelé pont JavaScript, afin de faciliter la communication
entre le contenu Web d'une WebView et une application Android hôte.
Cela permet aux développeurs Web d'utiliser JavaScript pour accéder aux fonctionnalités de la plate-forme native (telles que l'appareil photo, le système de fichiers ou les capteurs matériels avancés) que les API Web standards ne fournissent normalement pas.
Cas d'utilisation
L'implémentation d'un pont JavaScript permet différents scénarios d'intégration dans lesquels le contenu Web nécessite un accès plus approfondi au système d'exploitation Android. Voici quelques exemples :
- Intégration de la plate-forme : déclenchement de composants d'interface utilisateur Android natifs (par
exemple, des invites biométriques,
BottomSheetDialog) à partir d'une page Web. - Performances : déchargement des tâches de calcul lourdes vers du code Java ou Kotlin natif.
- Persistance des données : accès aux bases de données chiffrées locales ou aux préférences partagées.
- Transferts de données volumineux : transmission de fichiers multimédias ou de structures de données complexes entre l'application et le moteur de rendu Web.
Mécanismes de communication
Android propose trois générations principales d'API pour établir un pont natif. Bien qu'elles soient toutes encore disponibles, elles diffèrent considérablement en termes de sécurité, d'usabilité et de performances.
Utiliser addWebMessageListener (recommandé)
addWebMessageListener est l'approche la plus moderne et recommandée pour la communication entre le contenu Web et le code de l'application native. Elle combine la facilité d'utilisation de l'interface JavaScript avec la sécurité du système de messagerie.
Fonctionnement : l'application ajoute un écouteur avec un nom spécifique et un ensemble de
règles d'origine autorisées. La WebView s'assure ensuite que l'objet JavaScript est présent dans le champ d'application global (window.objectName) à partir du moment où la page commence à se charger.
Initialisation : pour vous assurer que la WebView injecte l'objet JavaScript avant
l'exécution d'un script, vous devez appeler addWebMessageListener avant d'accéder à la
page (par exemple, en appelant WebViewCompat.navigate ou loadUrl).
Caractéristiques principales :
Sécurité et confiance : contrairement aux API héritées, cette méthode nécessite un
Set<String>deallowedOriginRuleslors de l'initialisation. Il s'agit du principal mécanisme d'établissement de la confiance.Lorsque vous spécifiez une origine approuvée, telle que
https://example.com, la WebView garantit qu'elle n'expose les objets JavaScript injectés qu'aux pages Web chargées à partir de cette origine exacte.Le rappel de l'écouteur natif reçoit un paramètre
sourceOriginavec chaque message. Vous pouvez l'utiliser pour vérifier l'origine exacte de l'expéditeur si votre pont accepte plusieurs origines autorisées.Étant donné que la WebView applique strictement ces vérifications d'origine au niveau de la plate-forme, votre application peut généralement s'appuyer sur les messages reçus d'une
sourceOriginapprouvée comme étant véridiques, ce qui élimine le besoin d'une validation rigoureuse de la charge utile dans la plupart des implémentations standards.- La WebView fait correspondre les règles au schéma (HTTP/HTTPS), à l'hôte et au port.
- La WebView ignore les chemins d'accès. Par exemple,
https://example.comautorisehttps://example.com/loginethttps://example.com/home. - La WebView limite strictement les caractères génériques au début de l'hôte pour les sous-domaines. Par exemple,
https://*.example.comcorrespond àhttps://foo.example.com, mais pas àhttps://example.com. Si vous devez faire correspondre à la foishttps://example.comet ses sous-domaines, vous devez ajouter chaque règle d'origine séparément à la liste d'autorisation (par exemple,"https://example.com", "https://*.example.com"). Vous ne pouvez pas utiliser de caractères génériques pour le schéma ni au milieu d'un domaine.
Cela limite le pont aux domaines validés, empêchant ainsi l'exécution de code natif par du contenu tiers non autorisé ou des iFrames injectés.
Prise en charge de plusieurs frames : fonctionne sur tous les frames qui correspondent aux règles d'origine.
Threading : le rappel de l'écouteur s'exécute sur le thread principal (UI) de l'application. Si votre pont doit gérer un traitement de données complexe, une analyse JSON ou des recherches dans une base de données, vous devez décharger ce travail sur un thread en arrière-plan pour éviter de bloquer l'interface utilisateur de l'application avec une erreur "L'application ne répond pas" (ANR).
Bidirectionnel : lorsque la page Web envoie un message, l’application reçoit un
JavaScriptReplyProxyqu’elle peut utiliser pour renvoyer des messages à ce frame spécifique. Vous pouvez conserver cet objetreplyProxyet l'utiliser à tout moment pour envoyer n'importe quel nombre de messages à la page, et pas seulement pour répondre à chaque message individuel envoyé par la page. Si le frame d'origine quitte la page ou est détruit, les messages envoyés à l'aide depostMessage()sur le proxy sont ignorés en mode silencieux.Initiation côté application : bien que la page Web doive toujours lancer le canal de communication avec l'application, l'application native peut inviter unilatéralement la page Web à démarrer ce processus. L'application native peut communiquer avec la page Web à l'aide de
addDocumentStartJavaScript()(pour évaluer JavaScript avant le chargement de la page) ou deevaluateJavaScript()(pour évaluer JavaScript une fois la page chargée).
Limitation : cette API envoie des données sous forme de chaînes ou de tableaux byte[]. Pour les structures de données plus complexes, telles que les objets JSON, vous devez les sérialiser dans l'un de ces formats, puis les désérialiser de l'autre côté pour reconstruire la structure de données.
Exemple d'utilisation :
Pour comprendre la séquence complète d'un échange de messages bidirectionnel, les événements se déroulent dans l'ordre suivant :
- Initiation (application) : l'application native enregistre l'écouteur avec
addWebMessageListeneret lance la navigation sur les pages (par exemple, avecWebViewCompat.navigateouloadUrl). - Envoi de message (Web) : le code JavaScript de la page Web appelle
myObject.postMessage(message)pour lancer la communication. - Réception et réponse au message (application) : l'application reçoit le message dans le
rappel de l'écouteur et répond à l'aide de
replyProxy.postMessage()fourni. - Réception de la réponse (Web) : la page Web reçoit la réponse asynchrone dans la
myObject.onmessage()fonction de rappel.
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);
}
Le code JavaScript suivant illustre l'implémentation côté client de addWebMessageListener, ce qui permet au contenu Web de recevoir des messages de l'application native et d'envoyer ses propres messages via le proxy myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
Utiliser postWebMessage (alternative)
Android a introduit cette fonctionnalité pour fournir une alternative asynchrone basée sur la messagerie, semblable à window.postMessage du Web.
Fonctionnement : l'application utilise WebViewCompat.postWebMessage pour envoyer une charge utile
au frame principal de la page Web. Pour établir un canal de communication
bidirectionnel, vous pouvez créer un WebMessageChannel et transmettre l'un de ses
ports avec le message au contenu Web.
Caractéristiques :
- Asynchrone : comme
addWebMessageListener, cette méthode utilise la messagerie asynchrone, ce qui garantit que la page Web reste réactive aux interactions de l'utilisateur pendant que l'application traite les données en arrière-plan. - Connaissance de l'origine : vous pouvez spécifier un
targetOriginpour vous assurer que la WebView ne fournit des données qu'à un site Web approuvé.
Limites :
- Champ d'application : cette API limite la communication au frame principal. Elle n'est pas compatible avec l'adressage direct ni l'envoi de messages aux iFrames.
- Restrictions URI : vous ne pouvez pas utiliser cette méthode pour le contenu chargé à l'aide des URI
data:, des URIfile:ou deloadData(), sauf si vous spécifiez « * » comme origine cible. Cela permet à n'importe quelle page de recevoir le message. - Risque d'identité : il n'existe aucun moyen clair pour le contenu Web de vérifier l' identité de l'expéditeur. Un message reçu par la page Web peut provenir de votre application native ou d'un autre iFrame.
Utilisez cette méthode lorsque vous avez besoin d'un canal asynchrone simple pour les données basées sur des chaînes dans les versions antérieures d'Android qui ne sont pas compatibles avec addWebMessageListener.
Utiliser addJavascriptInterface (hérité)
La méthode la plus ancienne consiste à injecter directement une instance d'objet natif dans la WebView.
Fonctionnement : vous définissez une classe Kotlin ou Java, annotez les méthodes autorisées
avec @JavascriptInterface et ajoutez une instance de la classe à la
WebView à l'aide de addJavascriptInterface(Object, String).
Caractéristiques :
- Synchrone : l'environnement d'exécution JavaScript se bloque jusqu'à ce que la méthode de votre code Android renvoie une valeur.
- Sécurité des threads : le système appelle des méthodes sur un thread en arrière-plan, ce qui nécessite une synchronisation minutieuse côté Kotlin ou Java.
- Risque de sécurité : par défaut,
addJavascriptInterfaceest disponible pour chaque frame de la WebView, y compris les iFrames. Il ne dispose pas de contrôle d'accès basé sur l'origine. En raison du comportement asynchrone de WebView, il n'est pas possible de déterminer en toute sécurité l'URL du frame qui appelle votre interface. Vous ne devez pas vous appuyer sur des méthodes telles queWebView.getUrl()pour la vérification de la sécurité, car elles ne sont pas garanties d'être précises et n'indiquent pas quel frame spécifique a effectué la requête.
Résumé des mécanismes
Le tableau suivant fournit une comparaison rapide des trois principaux mécanismes d'implémentation de pont natif :
| Méthode | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| Implémentation | Asynchrone (écouteur sur le thread principal) | Asynchrone | Synchrone |
| Sécurité | La plus élevée (basée sur une liste d'autorisation) | Élevée (connaissance de l'origine) | Faible (aucune vérification d'origine) |
| Complexité | Modérée | Modérée | Simple |
| Direction | Bidirectionnelle | Bidirectionnelle | Web vers application |
| Version minimale de WebView | Version 82 (et Jetpack Webkit 1.3.0) | Version 45 (et Jetpack Webkit 1.1.0) | Toutes les versions |
| Recommandé | Oui | Non | Non |
Gérer les transferts de données volumineux
Vous devez gérer la mémoire avec soin lorsque vous transférez des charges utiles volumineuses, telles que des chaînes de plusieurs mégaoctets ou des fichiers binaires, pour éviter les erreurs "L'application ne répond pas" (ANR) ou les plantages sur les appareils 32 bits. Cette section présente les différentes techniques et limites associées au transfert de quantités importantes de données entre l'application hôte et le contenu Web.
Transférer des données binaires avec des tableaux d'octets
Avec la classe WebMessageCompat, vous pouvez envoyer directement des tableaux byte[]
au lieu de sérialiser des données binaires dans des chaînes Base64. Étant donné que Base64 ajoute environ 33% de surcharge à la taille des données, cette méthode est beaucoup plus efficace en termes de mémoire et plus rapide.
- Avantage binaire : transférez des données binaires telles que des fichiers image ou audio entre votre application native et le contenu Web.
- Limitation : même avec des tableaux d'octets, le système copie les données sur la limite de communication interprocessus (IPC) entre l'application et le processus isolé que WebView utilise pour afficher le contenu Web. Cela consomme toujours beaucoup de mémoire pour les fichiers très volumineux.
Les exemples de code suivants montrent comment configurer addWebMessageListener côté application native pour recevoir les messages marqués avec WebMessageCompat.TYPE_ARRAY_BUFFER et éventuellement répondre avec des données binaires en recherchant 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
);
}
}
Le code JavaScript suivant illustre l'implémentation côté client de addWebMessageListener, ce qui permet au contenu Web d'envoyer et de recevoir des données binaires (ArrayBuffer) vers et depuis l'application native à l'aide du proxy window.myBridge injecté dans l'exemple précédent.
// 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]);
}
};
}
Chargement de données à grande échelle efficace
Pour les fichiers très volumineux (> 10 Mo), utilisez la méthode shouldInterceptRequest pour
diffuser les données en streaming :
- La page Web lance un appel
fetch()à une URL personnalisée et réservée. Par exemple,https://app.local/large-file. - L'application Android intercepte cette requête dans
WebViewClient.shouldInterceptRequest. - L'application renvoie les données sous forme d'
InputStream.
Cela permet de diffuser les données en streaming par blocs plutôt que de charger l'intégralité de la charge utile en mémoire en même temps.
La fonction JavaScript suivante illustre le code côté client pour charger efficacement un fichier binaire volumineux à partir de l'application native à l'aide d'un appel fetch() standard à une URL personnalisée et réservée.
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);
}
}
Les exemples de code suivants illustrent le côté application native, à l'aide de la méthode WebViewClient.shouldInterceptRequest en Kotlin et en Java, pour diffuser un fichier binaire volumineux en interceptant une URL réservée personnalisée demandée par le contenu 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);
}
});
Suivre les recommandations de sécurité
Pour protéger votre application et les données utilisateur, suivez ces consignes lors de l'implémentation d'un pont :
Appliquer le protocole HTTPS : pour vous assurer qu'un contenu tiers malveillant ne peut pas appeler la logique native de votre application, n'autorisez la communication qu'avec des origines sécurisées.
S'appuyer sur les règles d'origine : la meilleure façon de gérer la confiance consiste à définir strictement vos
allowedOriginRuleset à vérifier lasourceOriginfournie dans le rappel de message. Évitez d'utiliser le caractère générique complet (*), qui correspond à toutes les origines, comme seule règle d'origine, sauf en cas d'absolue nécessité. L'utilisation de caractères génériques pour les sous-domaines (par exemple,*.example.com) reste valide et sécurisée pour la mise en correspondance de plusieurs sous-domaines (par exemple,foo.example.com,bar.example.com).Remarque : Bien que les règles d'origine protègent contre les sites Web tiers malveillants et les iFrames masqués, elles ne peuvent pas protéger contre les failles de script intersite (XSS) au sein de votre propre domaine approuvé. Par exemple, si votre page Web affiche du contenu généré par l'utilisateur et est vulnérable au XSS stocké, un pirate informatique peut exécuter un script agissant comme votre origine approuvée. Envisagez d'appliquer une validation aux charges utiles des messages avant d'exécuter des opérations sensibles sur la plate-forme native.
Minimiser la surface d'attaque : n'exposez que les méthodes ou les données spécifiques dont la page Web a besoin.
Vérifier les fonctionnalités au moment de l'exécution : les API de pont récentes, y compris
addWebMessageListener, font partie de la bibliothèque Jetpack Webkit. Par conséquent, vérifiez toujours la compatibilité à l'aide deWebViewFeature.isFeatureSupported()avant de les appeler.