בדף הזה נסביר על השיטות השונות ועל השיטות המומלצות להקמת גשר מקורי, שנקרא גם גשר JavaScript, כדי לאפשר תקשורת בין תוכן מהאינטרנט ב-WebView לבין אפליקציה ל-Android מארחת.
היא מאפשרת למפתחי אתרים להשתמש ב-JavaScript כדי לגשת לתכונות של פלטפורמה מקומית – כמו המצלמה, מערכת הקבצים או חיישני חומרה מתקדמים – שבדרך כלל לא מסופקות על ידי ממשקי API רגילים לאינטרנט.
תרחישים לדוגמה
הטמעה של גשר JavaScript מאפשרת תרחישי שילוב שונים שבהם תוכן אינטרנט דורש גישה עמוקה יותר למערכת ההפעלה Android. הנה כמה דוגמאות:
- שילוב פלטפורמות: הפעלה של רכיבי ממשק משתמש מובנים של Android (לדוגמה, הנחיות ביומטריות,
BottomSheetDialog) מדף אינטרנט. - ביצועים: העברת משימות חישוביות כבדות לקוד Java או Kotlin מקורי.
- התמדת נתונים: גישה למסדי נתונים מוצפנים מקומיים או להעדפות משותפות.
- העברות נתונים גדולות: העברת קובצי מדיה או מבני נתונים מורכבים בין האפליקציה לבין רכיב העיבוד של הדף.
מנגנוני תקשורת
מערכת Android מציעה שלושה דורות עיקריים של ממשקי API ליצירת גשר מקורי. כולם עדיין זמינים, אבל יש ביניהם הבדלים משמעותיים מבחינת אבטחה, שימושיות וביצועים.
שימוש ב-addWebMessageListener (מומלץ)
addWebMessageListener היא הגישה המודרנית והמומלצת ביותר לתקשורת בין תוכן מהאינטרנט לבין קוד אפליקציית נייטיב. הוא משלב בין קלות השימוש בממשק JavaScript לבין האבטחה של מערכת העברת ההודעות.
איך זה עובד: האפליקציה מוסיפה מאזין עם שם ספציפי וקבוצה של כללי מקור מותרים. לאחר מכן, רכיב ה-WebView מוודא שאובייקט ה-JavaScript נמצא בהיקף הגלובלי (window.objectName) מהרגע שבו הדף מתחיל להיטען.
הפעלה: כדי לוודא שרכיב ה-WebView מזריק את אובייקט ה-JavaScript לפני הפעלת סקריפט כלשהו, צריך לקרוא לפונקציה addWebMessageListener לפני שקוראים לפונקציה loadUrl().
התכונות העיקריות:
אבטחה ואמון: בניגוד לממשקי API מדור קודם, השיטה הזו מחייבת
Set<String>שלallowedOriginRulesבמהלך האתחול. זהו המנגנון העיקרי ליצירת אמון.כשמציינים מקור מהימן, כמו
https://example.com, רכיב ה-WebView מבטיח שהוא יחשוף את אובייקטי ה-JavaScript המוזרקים רק לדפי אינטרנט שנטענו בדיוק מהמקור הזה.הקריאה החוזרת של מאזין מקורי מקבלת פרמטר
sourceOriginעם כל הודעה. אפשר להשתמש בזה כדי לאמת את המקור המדויק של השולח אם הגשר תומך בכמה מקורות מורשים.מכיוון ש-WebView אוכף את בדיקות המקור האלה באופן קפדני ברמת הפלטפורמה, האפליקציה יכולה להסתמך בדרך כלל על הודעות שמתקבלות מ-
sourceOriginמהימן כהודעות אמיתיות, ולכן אין צורך באימות קפדני של מטען ייעודי (payload) ברוב ההטמעות הרגילות.- WebView מתאים כללים לסכמה (HTTP/HTTPS), למארח ולפורט.
- WebView מתעלם מנתיבים. לדוגמה,
https://example.comמאפשרתhttps://example.com/loginו-https://example.com/home. - ב-WebView, התווים הכלליים מוגבלים להתחלה של המארח עבור תת-דומיינים. לדוגמה,
https://*.example.comתואם ל-https://foo.example.comאבל לא ל-https://example.com. אם אתם צריכים להתאים גם אתhttps://example.comוגם את תתי-הדומיינים שלו, אתם צריכים להוסיף כל כלל מקור בנפרד לרשימת ההיתרים (לדוגמה,"https://example.com", "https://*.example.com"). אי אפשר להשתמש בתווים כלליים בסכימה או באמצע של דומיין.
ההגדרה הזו מגבילה את הגשר לדומיינים מאומתים, ומונעת מתוכן של צד שלישי לא מורשה או מ-iframes מוזרקים להריץ קוד Native.
תמיכה בריבוי מסגרות: פועלת בכל המסגרות שתואמות לכללי המקור.
Threading: The listener callback runs on the application's main (UI) thread. אם הגשר צריך לטפל בעיבוד נתונים מורכב, בניתוח של JSON או בחיפושים במסד נתונים, צריך להעביר את העבודה הזו לשרשור ברקע כדי למנוע את קפיאת ממשק המשתמש של האפליקציה עם השגיאה 'האפליקציה לא מגיבה' (ANR).
דו-כיווני: כשדף האינטרנט שולח הודעה, האפליקציה מקבלת
JavaScriptReplyProxyשאפשר להשתמש בו כדי לשלוח הודעות חזרה למסגרת הספציפית הזו. אפשר לשמור את אובייקטreplyProxyהזה ולהשתמש בו בכל שלב כדי לשלוח מספר כלשהו של הודעות לדף, ולא רק כדי להשיב לכל הודעה שהדף שולח. אם המסגרת המקורית עוברת למקום אחר או נהרסת, המערכת מתעלמת בשקט מהודעות שנשלחות באמצעותpostMessage()בשרת ה-proxy.הפעלה מצד האפליקציה: למרות שדף האינטרנט תמיד צריך להפעיל את ערוץ התקשורת עם האפליקציה, אפליקציית הנייטיב יכולה להנחות את דף האינטרנט להתחיל את התהליך הזה באופן חד-צדדי. אפליקציית הנייטיב יכולה לתקשר עם דף האינטרנט באמצעות
addDocumentStartJavaScript()(כדי להעריך JavaScript לפני טעינת הדף) אוevaluateJavaScript()(כדי להעריך JavaScript אחרי טעינת הדף).
הגבלה: ה-API הזה שולח נתונים כמחרוזות או כמערכים של byte[]. לגבי מבני נתונים מורכבים יותר, כמו אובייקטים של JSON, צריך לבצע סריאליזציה לאחד מהפורמטים האלה ואז לבצע דה-סריאליזציה בצד השני כדי לשחזר את מבנה הנתונים.
דוגמה לשימוש:
כדי להבין את הרצף המלא של חילופי הודעות דו-כיווניים, האירועים מתרחשים בסדר הבא:
- הפעלה (אפליקציה): אפליקציית הנייטיב רושמת את מאזין האירועים באמצעות
addWebMessageListenerוטוענת את דף האינטרנט באמצעותloadUrl(). - שליחת הודעה (אינטרנט): קריאות JavaScript בדף האינטרנט
myObject.postMessage(message)כדי להתחיל את התקשורת. - קבלת הודעות ושליחת תשובות (אפליקציה): האפליקציה מקבלת את ההודעה ב-callback של listener ועונה באמצעות
replyProxy.postMessage()שסופק. - קבלת תשובה (אינטרנט): דף האינטרנט מקבל את התשובה האסינכרונית בפונקציית הקריאה החוזרת
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);
}
קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של addWebMessageListener, שמאפשרת לתוכן האינטרנט לקבל הודעות מאפליקציית הנייטיב ולשלוח הודעות משלו דרך ה-proxy של myObject.
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
שימוש ב-postWebMessage (חלופה)
ב-Android הוצגה אפשרות כזו כדי לספק חלופה אסינכרונית שמבוססת על העברת הודעות, בדומה ל-window.postMessage באינטרנט.
איך זה עובד: האפליקציה משתמשת ב-WebViewCompat.postWebMessage כדי לשלוח מטען ייעודי (payload)
למסגרת הראשית של דף האינטרנט. כדי ליצור ערוץ תקשורת דו-כיווני, אפשר ליצור WebMessageChannel ולהעביר את אחד הפורטים שלו עם ההודעה לתוכן האינטרנט.
מאפיינים:
- אסינכרוני: כמו
addWebMessageListener, השיטה הזו משתמשת בהעברת הודעות אסינכרונית, שמבטיחה שדף האינטרנט ימשיך להגיב לאינטראקציות של המשתמשים בזמן שהאפליקציה מעבדת נתונים ברקע. - מודע למקור: אפשר לציין
targetOriginכדי לוודא שרכיב ה-WebView יעביר נתונים רק לאתר מהימן.
מגבלות:
- היקף: ה-API הזה מגביל את התקשורת למסגרת הראשית. הוא לא תומך בפנייה ישירה או בשליחת הודעות ל-iframes.
- הגבלות על URI: אי אפשר להשתמש בשיטה הזו לתוכן שנטען באמצעות מזהי URI מסוג
data:, מזהי URI מסוגfile:אוloadData(), אלא אם מציינים '*' כמקור היעד. כך כל דף יוכל לקבל את ההודעה. - סיכון לזהות: אין דרך ברורה לאמת את זהות השולח בתוכן האינטרנט. יכול להיות שהודעה שמתקבלת בדף אינטרנט הגיעה מאפליקציית נייטיב או מ-iframe אחר.
משתמשים בשיטה הזו כשצריך ערוץ פשוט ואסינכרוני לנתונים מבוססי-מחרוזות בגרסאות קודמות של Android שלא תומכות ב-addWebMessageListener.
שימוש ב-addJavascriptInterface (בגרסה הקודמת)
השיטה הכי ישנה כוללת הזרקה של מופע אובייקט מקורי ישירות ל-WebView.
איך זה עובד: מגדירים מחלקה ב-Kotlin או ב-Java, מוסיפים את ההערה @JavascriptInterface לשיטות המותרות ומוסיפים מופע של המחלקה ל-WebView באמצעות addJavascriptInterface(Object, String).
מאפיינים:
- סינכרוני: סביבת ההפעלה של JavaScript נחסמת עד שהשיטה בקוד Android מחזירה ערך.
- Thread safety: המערכת מפעילה שיטות ב-thread ברקע, ולכן נדרש סנכרון זהיר בצד של Kotlin או Java.
- סיכון אבטחה: כברירת מחדל,
addJavascriptInterfaceזמין לכל מסגרת ב-WebView, כולל iframe. אין בו בקרת גישה שמבוססת על מקור. בגלל ההתנהגות האסינכרונית של WebView, אי אפשר לקבוע באופן בטוח את כתובת ה-URL של המסגרת שמפעילה את הממשק. אסור להסתמך על שיטות כמוWebView.getUrl()לאימות אבטחה, כי אין ערובה לכך שהן מדויקות, והן לא מציינות איזה פריים ספציפי של iframe שלח את הבקשה.
סיכום המנגנונים
בטבלה הבאה מופיעה השוואה מהירה בין שלושת המנגנונים העיקריים להטמעה של גשרים מקוריים:
| שיטה | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| הטמעה | אסינכרוני (Listener ב-thread הראשי) | אסינכרוני | סינכרוני |
| אבטחה | הכי גבוהה (על בסיס רשימת היתרים) | גבוהה (מודעת למקור) | נמוכה (ללא בדיקות מקור) |
| מורכבות | בינונית | בינונית | פשוט |
| כיוון | דו-כיווני | דו-כיווני | צירוף משתמשים לאפליקציה דרך האתר |
| גרסת WebView מינימלית | גרסה 82 (ו-Jetpack Webkit 1.3.0) | גרסה 45 (ו-Jetpack Webkit 1.1.0) | כל הגירסאות |
| מומלץ | כן | לא | לא |
טיפול בהעברות נתונים גדולות
כשמעבירים נתונים גדולים, כמו מחרוזות של כמה מגה-בייט או קבצים בינאריים, צריך לנהל את הזיכרון בקפידה כדי למנוע שגיאות מסוג ANR (האפליקציה לא מגיבה) או קריסות במכשירים עם 32 ביט. בקטע הזה נדון בטכניקות השונות ובמגבלות שקשורות להעברת כמויות גדולות של נתונים בין אפליקציית המארח לבין תוכן האינטרנט.
העברת נתונים בינאריים באמצעות מערכים של בייטים
בעזרת המחלקה WebMessageCompat, אפשר לשלוח מערכים של byte[] ישירות
במקום לבצע סריאליזציה של נתונים בינאריים למחרוזות Base64. הקידוד ב-Base64 מוסיף תקורת נתונים של כ-33%, ולכן השיטה הזו חסכונית יותר בזיכרון ומהירה יותר.
- יתרון בינארי: העברת נתונים בינאריים כמו קובצי תמונות או אודיו בין אפליקציית הנייטיב לבין תוכן מהאינטרנט.
- מגבלה: גם עם מערכי בייטים, המערכת מעתיקה נתונים מעבר לגבול של תקשורת בין תהליכים (IPC) בין האפליקציה לתהליך המבודד שבו WebView משתמש כדי לעבד את תוכן האינטרנט. הפעולה הזו עדיין צורכת כמות משמעותית של זיכרון עבור קבצים גדולים מאוד.
בדוגמאות הקוד הבאות אפשר לראות איך מגדירים את addWebMessageListener בצד של אפליקציית הנייטיב כדי לקבל הודעות שמסומנות ב-WebMessageCompat.TYPE_ARRAY_BUFFER, ואיך אפשר להשיב עם נתונים בינאריים על ידי בדיקה של 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
);
}
}
קוד ה-JavaScript הבא מדגים את ההטמעה בצד הלקוח של addWebMessageListener, שמאפשרת לתוכן מהאינטרנט לשלוח ולקבל נתונים בינאריים (ArrayBuffer) אל אפליקציית הנייטיב וממנה באמצעות ה-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]);
}
};
}
טעינה יעילה של נתונים בקנה מידה גדול
לקבצים גדולים מאוד (מעל 10MB), משתמשים בשיטה shouldInterceptRequest כדי להזרים נתונים:
- דף האינטרנט יוזם קריאה (call) לכתובת URL מותאמת אישית של placeholder.
fetch()לדוגמה,https://app.local/large-file. - אפליקציית Android מיירטת את הבקשה הזו ב-
WebViewClient.shouldInterceptRequest. - האפליקציה מחזירה את הנתונים כ-
InputStream.
האפשרות הזו מאפשרת להזרים נתונים במקטעים במקום לטעון את כל מטען הנתונים לזיכרון בבת אחת.
פונקציית JavaScript הבאה מדגימה את הקוד בצד הלקוח לטעינה יעילה של קובץ בינארי גדול מאפליקציית נייטיב באמצעות קריאה רגילה fetch() לכתובת URL של פלייסהולדר בהתאמה אישית.
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);
}
}
בדוגמאות הקוד הבאות מוצג הצד של אפליקציית הנייטיב, באמצעות השיטה WebViewClient.shouldInterceptRequest ב-Kotlin וב-Java, להזרמת קובץ בינארי גדול על ידי יירוט של כתובת URL של פלייסהולדר בהתאמה אישית שנדרשת על ידי תוכן מהאינטרנט.
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);
}
});
פעולה בהתאם להמלצות האבטחה
כדי להגן על האפליקציה ועל נתוני המשתמשים, חשוב לפעול לפי ההנחיות הבאות כשמטמיעים גשר:
אכיפת HTTPS: כדי לוודא שתוכן זדוני של צד שלישי לא יוכל להפעיל את הלוגיקה המקורית של האפליקציה, צריך לאפשר תקשורת רק עם מקורות מאובטחים.
להסתמך על כללים לגבי מקורות: הדרך הכי טובה להתמודד עם בעיות של מהימנות היא להגדיר בקפדנות את
allowedOriginRulesולבדוק אתsourceOriginשמופיע בקריאה החוזרת של ההודעה. מומלץ להימנע משימוש בתו כללי מלא (*), שתואם לכל המקורות, ככלל המקור היחיד, אלא אם זה ממש הכרחי. שימוש בתווים כלליים לחיפוש בתתי-דומיינים (לדוגמה,*.example.com) עדיין תקף ומאובטח להתאמה למספר תתי-דומיינים (לדוגמה,foo.example.com,bar.example.com).הערה: כללים לגבי מקורות מגנים מפני אתרים זדוניים של צד שלישי ומפני iframe מוסתרים, אבל הם לא יכולים להגן מפני פרצות אבטחה מסוג XSS (cross-site scripting) בדומיין המהימן שלכם. לדוגמה, אם דף אינטרנט מציג תוכן שנוצר על ידי משתמשים והוא פגיע ל-XSS מאוחסן, תוקף יכול להריץ סקריפט שפועל כמקור מהימן. מומלץ להחיל אימות על נתוני התוכן של ההודעות לפני שמבצעים פעולות רגישות בפלטפורמה המקורית.
צמצום שטח הפנים: חשיפה רק של השיטות או הנתונים הספציפיים שדף האינטרנט דורש.
בדיקת תכונות בזמן ריצה: ממשקי Bridge API חדשים, כולל
addWebMessageListener, הם חלק מספריית Jetpack Webkit. לכן, תמיד כדאי לבדוק אם יש תמיכה באמצעותWebViewFeature.isFeatureSupported()לפני שמתקשרים אליהם.