หน้านี้จะกล่าวถึงวิธีการต่างๆ และแนวทางปฏิบัติแนะนำในการสร้าง
บริดจ์เนทีฟ หรือที่เรียกว่าบริดจ์ JavaScript เพื่ออำนวยความสะดวกในการสื่อสาร
ระหว่างเนื้อหาเว็บใน WebView กับแอปพลิเคชัน Android โฮสต์
ซึ่งช่วยให้นักพัฒนาเว็บใช้ JavaScript เพื่อเข้าถึงฟีเจอร์ของแพลตฟอร์มเนทีฟ เช่น กล้อง ระบบไฟล์ หรือเซ็นเซอร์ฮาร์ดแวร์ขั้นสูง ซึ่งโดยปกติแล้ว Web API มาตรฐานจะไม่มีให้
กรณีการใช้งาน
การติดตั้งใช้งานบริดจ์ JavaScript ช่วยให้เกิดสถานการณ์การผสานรวมต่างๆ ที่เนื้อหาเว็บต้องเข้าถึงระบบปฏิบัติการ Android ในระดับที่ลึกขึ้น ตัวอย่างมีดังนี้
- การผสานรวมแพลตฟอร์ม: การเรียกใช้คอมโพเนนต์ UI ของ Android แบบเนทีฟ (เช่น พรอมต์ไบโอเมตริก
BottomSheetDialog) จากหน้าเว็บ - ประสิทธิภาพ: การส่งต่อภาระงานด้านการคำนวณที่หนักหน่วงไปยังโค้ด Java หรือ Kotlin แบบเนทีฟ
- การคงอยู่ของข้อมูล: การเข้าถึงฐานข้อมูลที่เข้ารหัสในเครื่องหรือค่ากำหนดที่แชร์
- การโอนข้อมูลขนาดใหญ่: การส่งไฟล์สื่อหรือโครงสร้างข้อมูลที่ซับซ้อน ระหว่างแอปกับเครื่องมือแสดงผลบนเว็บ
กลไกการสื่อสาร
Android มี API หลัก 3 รุ่นเพื่อสร้างบริดจ์เนทีฟ แม้ว่าทั้ง 3 รูปแบบจะยังคงใช้งานได้ แต่ก็มีความแตกต่างกันอย่างมากในด้านความปลอดภัย ความสามารถในการใช้งาน และประสิทธิภาพ
ใช้ addWebMessageListener (แนะนำ)
addWebMessageListener เป็นแนวทางที่ทันสมัยที่สุดและแนะนำสำหรับการสื่อสารระหว่างเนื้อหาเว็บกับโค้ดแอปที่มาพร้อมเครื่อง โดยผสานความสะดวก
ในการใช้อินเทอร์เฟซ JavaScript เข้ากับความปลอดภัยของระบบการรับส่งข้อความ
วิธีการทำงาน: แอปจะเพิ่ม Listener ที่มีชื่อเฉพาะและชุดกฎต้นทางที่อนุญาต
จากนั้น WebView จะตรวจสอบว่าออบเจ็กต์ JavaScript อยู่ในขอบเขตส่วนกลาง (window.objectName) ตั้งแต่หน้าเว็บเริ่มโหลด
การเริ่มต้น: หากต้องการให้ WebView แทรกออบเจ็กต์ JavaScript ก่อนที่สคริปต์จะทํางาน คุณต้องเรียกใช้ addWebMessageListener ก่อนไปยังหน้าเว็บ (เช่น เรียกใช้ WebViewCompat.navigate หรือ loadUrl)
ฟีเจอร์หลัก
ความปลอดภัยและความน่าเชื่อถือ: เมธอดนี้ต้องใช้
Set<String>ของallowedOriginRulesในระหว่างการเริ่มต้น ซึ่งแตกต่างจาก API เดิม ซึ่งเป็น กลไกหลักในการสร้างความไว้วางใจเมื่อคุณระบุต้นทางที่เชื่อถือได้ เช่น
https://example.comWebView จะรับประกันว่าระบบจะแสดงออบเจ็กต์ JavaScript ที่แทรกไปยัง หน้าเว็บที่โหลดจากต้นทางนั้นๆ เท่านั้นCallback ของ Listener ดั้งเดิมจะได้รับพารามิเตอร์
sourceOriginพร้อมกับข้อความทุกข้อความ คุณสามารถใช้พารามิเตอร์นี้เพื่อยืนยันแหล่งที่มาที่แน่นอนของผู้ส่งได้หาก บริดจ์รองรับแหล่งที่มาที่อนุญาตหลายรายการเนื่องจาก WebView บังคับใช้การตรวจสอบแหล่งที่มาเหล่านี้อย่างเคร่งครัดที่ระดับแพลตฟอร์ม แอปของคุณจึงมักจะเชื่อถือข้อความที่ได้รับจาก
sourceOriginที่เชื่อถือได้ ว่าถูกต้อง ซึ่งช่วยลดความจำเป็นในการตรวจสอบเพย์โหลดอย่างเข้มงวดในการติดตั้งใช้งานมาตรฐานส่วนใหญ่- 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") คุณไม่สามารถใช้อักขระไวด์การ์ดสำหรับรูปแบบหรือตรงกลางโดเมน
ซึ่งจะจำกัดการเชื่อมต่อกับโดเมนที่ยืนยันแล้ว เพื่อป้องกันไม่ให้เนื้อหาของบุคคลที่สามที่ไม่ได้รับอนุญาต หรือ iframe ที่แทรกทำงานโค้ดแบบเนทีฟ
รองรับหลายเฟรม: ทำงานในทุกเฟรมที่ตรงกับกฎต้นทาง
การทำงานแบบเธรด: การเรียกกลับของ Listener จะทำงานในเธรดหลัก (UI) ของแอปพลิเคชัน หากบริดจ์ต้องจัดการการประมวลผลข้อมูลที่ซับซ้อน การแยกวิเคราะห์ JSON หรือการค้นหาฐานข้อมูล คุณต้องลดภาระการทำงานดังกล่าวไปยังเทรดเบื้องหลังเพื่อป้องกันไม่ให้ UI ของแอปพลิเคชันหยุดทำงานเนื่องจากข้อผิดพลาด "แอปไม่ตอบสนอง" (ANR)
สองทิศทาง: เมื่อหน้าเว็บส่งข้อความ แอปจะได้รับ
JavaScriptReplyProxyที่ใช้ส่งข้อความกลับไปยังเฟรมนั้นๆ ได้ คุณสามารถเก็บออบเจ็กต์replyProxyนี้ไว้และใช้ได้ทุกเมื่อเพื่อส่งข้อความจำนวนเท่าใดก็ได้ไปยังเพจ ไม่ใช่แค่เพื่อตอบกลับข้อความแต่ละรายการที่เพจส่งมา หากเฟรมต้นทางไปยังที่อื่น หรือถูกทำลาย ระบบจะเพิกเฉยต่อข้อความที่ส่งโดยใช้postMessage()ในพร็อกซี โดยไม่มีการแจ้งเตือนการเริ่มต้นฝั่งแอป: แม้ว่าหน้าเว็บจะต้องเริ่มต้นช่องทางการสื่อสารกับแอปเสมอ แต่แอปที่มาพร้อมเครื่องสามารถแจ้งให้หน้าเว็บเริ่มกระบวนการนี้ได้โดยฝ่ายเดียว แอปที่มาพร้อมเครื่องสามารถสื่อสารกับหน้าเว็บด้วย
addDocumentStartJavaScript()(เพื่อประเมิน JavaScript ก่อนที่หน้าเว็บจะโหลด) หรือevaluateJavaScript()(เพื่อประเมิน JavaScript หลังจากที่หน้าเว็บโหลดแล้ว)
ข้อจำกัด: API นี้จะส่งข้อมูลเป็นสตริงหรืออาร์เรย์ byte[] สำหรับโครงสร้างข้อมูลที่ซับซ้อนกว่า เช่น ออบเจ็กต์ JSON คุณต้องทำให้ข้อมูลนี้เป็นอนุกรมในรูปแบบใดรูปแบบหนึ่ง แล้วจึงยกเลิกการทำอนุกรมในอีกด้านหนึ่งเพื่อสร้างโครงสร้างข้อมูลขึ้นใหม่
ตัวอย่างการใช้งาน
หากต้องการทำความเข้าใจลำดับทั้งหมดของการแลกเปลี่ยนข้อความแบบ 2 ทาง เหตุการณ์ จะดำเนินการตามลำดับต่อไปนี้
- การเริ่มต้น (แอป): แอปที่มาพร้อมเครื่องจะลงทะเบียน Listener ด้วย
addWebMessageListenerและเริ่มการนำทางหน้าเว็บ (เช่น ด้วยWebViewCompat.navigateหรือloadUrl) - ส่งข้อความ (เว็บ): การเรียก JavaScript ของหน้าเว็บ
myObject.postMessage(message)เพื่อเริ่มการสื่อสาร - รับและตอบกลับข้อความ (แอป): แอปจะรับข้อความใน
การเรียกกลับของ Listener และตอบกลับโดยใช้
replyProxy.postMessage()ที่ระบุ - รับการตอบกลับ (เว็บ): หน้าเว็บจะได้รับการตอบกลับแบบอะซิงโครนัสในฟังก์ชัน Callback
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 ซึ่งช่วยให้เนื้อหาเว็บรับข้อความจาก
แอปที่มาพร้อมเครื่องและส่งข้อความของตัวเองผ่านพร็อกซี myObject ได้
myObject.onmessage = function(event) {
console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");
ใช้ postWebMessage (ทางเลือก)
Android เปิดตัวฟีเจอร์นี้เพื่อเป็นทางเลือกแบบไม่พร้อมกันที่อิงตามการรับส่งข้อความ
คล้ายกับ window.postMessage ของเว็บ
วิธีการทำงาน: แอปใช้ WebViewCompat.postWebMessage เพื่อส่งเพย์โหลด
ไปยังเฟรมหลักของหน้าเว็บ หากต้องการสร้างช่องทางการสื่อสารแบบ 2 ทาง
คุณสามารถสร้าง WebMessageChannel และส่งพอร์ตหนึ่งของ
พอร์ตนั้นพร้อมข้อความไปยังเนื้อหาเว็บ
ลักษณะ
- อะซิงโครนัส: เช่นเดียวกับ
addWebMessageListenerวิธีนี้ใช้ การรับส่งข้อความแบบอะซิงโครนัส ซึ่งช่วยให้หน้าเว็บยังคงตอบสนองต่อ การโต้ตอบของผู้ใช้ในขณะที่แอปประมวลผลข้อมูลในเบื้องหลัง - รับรู้ต้นทาง: คุณระบุ
targetOriginเพื่อให้มั่นใจว่า WebView จะส่งข้อมูลไปยังเว็บไซต์ที่เชื่อถือได้เท่านั้น
ข้อจำกัด
- ขอบเขต: API นี้จำกัดการสื่อสารไว้ที่เฟรมหลัก แต่ไม่รองรับการระบุที่อยู่หรือการส่งข้อความไปยัง iframe โดยตรง
- ข้อจำกัดของ URI: คุณไม่สามารถใช้วิธีนี้กับเนื้อหาที่โหลดโดยใช้ URI ของ
data:, URI ของfile:หรือloadData()เว้นแต่คุณจะระบุ "*" เป็นต้นทางเป้าหมาย การทำเช่นนี้จะทำให้ทุกหน้าได้รับข้อความ - ความเสี่ยงด้านข้อมูลประจำตัว: เนื้อหาเว็บไม่มีวิธีที่ชัดเจนในการยืนยันตัวตนของผู้ส่ง ข้อความที่หน้าเว็บได้รับอาจมาจากแอปที่มาพร้อมเครื่องหรือ iframe อื่น
ใช้วิธีนี้เมื่อต้องการช่องทางแบบไม่พร้อมกันอย่างง่ายสำหรับข้อมูลที่อิงตามสตริงใน Android เวอร์ชันก่อนหน้าที่ไม่รองรับ addWebMessageListener
ใช้ addJavascriptInterface (เดิม)
วิธีที่เก่าแก่ที่สุดคือการแทรกอินสแตนซ์ออบเจ็กต์ดั้งเดิมลงใน WebView โดยตรง
วิธีการทำงาน: คุณกำหนดคลาส Kotlin หรือ Java ใส่คำอธิบายประกอบให้กับเมธอดที่อนุญาตด้วย @JavascriptInterface และเพิ่มอินสแตนซ์ของคลาสลงใน WebView โดยใช้ addJavascriptInterface(Object, String)
ลักษณะ
- ซิงโครนัส: สภาพแวดล้อมการเรียกใช้ JavaScript จะบล็อกจนกว่าเมธอดในโค้ด Android จะส่งคืน
- ความปลอดภัยของเธรด: ระบบจะเรียกใช้เมธอดในเธรดเบื้องหลัง ซึ่งต้องมีการซิงค์อย่างระมัดระวังในฝั่ง Kotlin หรือ Java
- ความเสี่ยงด้านความปลอดภัย: โดยค่าเริ่มต้น
addJavascriptInterfaceจะพร้อมใช้งานใน ทุกเฟรมภายใน WebView รวมถึง iframe ไม่มีการควบคุมการเข้าถึงตามต้นทาง เนื่องจากลักษณะการทำงานแบบไม่พร้อมกันของ WebView จึงไม่สามารถ ระบุ URL ของเฟรมที่เรียกอินเทอร์เฟซของคุณได้อย่างปลอดภัย คุณต้องไม่ใช้วิธีการต่างๆ เช่นWebView.getUrl()เพื่อการยืนยันความปลอดภัย เนื่องจากไม่มีการรับประกันความถูกต้องและไม่ได้ระบุว่าเฟรมใดที่ส่งคำขอ
การแปลงและการบังคับประเภทข้อมูล
เมื่อใช้ addJavascriptInterface บริดจ์ Java ที่ใช้ Chromium จะแปลง
ประเภทข้อมูลระหว่างรันไทม์ JavaScript กับโค้ดแอป Android
กฎการบังคับต่อไปนี้ใช้กับพารามิเตอร์ของเมธอดและค่าที่ส่งคืน
การแมปประเภทพารามิเตอร์ (JavaScript เป็น Java)
เมื่อ JavaScript ส่งอาร์กิวเมนต์ไปยังเมธอด Java หรือ Kotlin ที่มีคำอธิบายประกอบ บริดจ์จะบังคับให้ค่า JavaScript เป็นประเภทพารามิเตอร์ Java ที่สอดคล้องกัน
| ประเภทพารามิเตอร์ Java | ค่าอาร์กิวเมนต์ JavaScript | พฤติกรรมการบังคับขู่เข็ญ |
|---|---|---|
byte, short, int, long |
ตัวเลข (จำนวนเต็ม) | ระบบจะส่งค่าไปยังประเภทจำนวนเต็มเป้าหมาย ค่าที่อยู่นอกขอบเขตจะวนรอบตามกฎการแคสต์ตัวเลขมาตรฐาน |
byte, short, int, long |
NaN |
บังคับให้เป็น 0 |
byte, short, int, long |
Infinity |
บังคับให้เป็น -1 สำหรับ byte และ short หรือ Integer.MAX_VALUE และ Long.MAX_VALUE สำหรับ int และ long |
float, double |
หมายเลข | แปลงประเภทเป็นค่าจุดลอยตัว Java ที่สอดคล้องกัน |
float, double |
NaN / Infinity |
บังคับเป็น Float.NaN, Double.NaN, Float.POSITIVE_INFINITY หรือ Double.POSITIVE_INFINITY |
char |
ตัวเลข (จำนวนเต็ม) | แปลงเป็น Code Point ของ Unicode ที่สอดคล้องกัน |
char |
ไม่ใช่จำนวนเต็ม NaN Infinity |
บังคับให้เป็น \u0000 |
boolean |
true / false |
บังคับให้เป็น true หรือ false ของ Java |
boolean |
ตัวเลข สตริง ออบเจ็กต์ | บังคับให้เป็น false (รวมถึงสตริงที่ไม่ว่างเปล่าและตัวเลขที่ไม่ใช่ 0) |
String |
สตริง | ระบบจะเก็บค่าสตริงไว้ |
String |
ตัวเลข บูลีน | จัดรูปแบบเป็นตัวแทนสตริง (เช่น "42", "true", "false") |
String |
null / undefined |
null จะบังคับให้เป็น Java null ส่วน undefined จะบังคับให้เป็นสตริงลิเทอรัล "undefined" |
String |
ออบเจ็กต์, ArrayBuffer, TypedArray | บังคับให้เป็นสตริงตามตัวอักษร "undefined" |
อาร์เรย์ดั้งเดิม (เช่น int[], byte[], boolean[]) หรือ String[] |
อาร์เรย์ ([...]) |
แปลงเป็นอาร์เรย์ Java แบบ 1 มิติของประเภทองค์ประกอบเป้าหมาย อาร์เรย์แบบกระจัดกระจายจะกรอกดัชนีที่ไม่ได้กำหนดด้วยค่าเริ่มต้น (0, false, null) |
อาร์เรย์ดั้งเดิม (เช่น int[], byte[]) |
TypedArray (Int8Array, Uint8Array, Int32Array, Float64Array) |
ระบบจะบังคับให้องค์ประกอบเป็นอาร์เรย์ดั้งเดิมของ Java ที่เกี่ยวข้อง |
อาร์เรย์หลายมิติ (เช่น int[][]) |
อาร์เรย์ที่ซ้อนกัน ([[...]]) |
ไม่รองรับ พารามิเตอร์อาร์เรย์หลายมิติจะประเมินเป็น null |
ArrayBuffer, DataView |
ArrayBuffer, DataView |
ไม่รองรับในรูปแบบอาร์เรย์ อินสแตนซ์ ArrayBuffer และ DataView จะประเมินเป็น null |
Object หรือคลาสที่กำหนดเอง |
ออบเจ็กต์ JavaScript ({...}) |
ไม่รองรับ ออบเจ็กต์ JavaScript ที่กำหนดเองจะประเมินเป็น null ใน Java |
Object หรือคลาสที่กำหนดเอง |
Wrapper ออบเจ็กต์ Java ที่แทรก | รองรับ (การส่งกลับ) ส่งอินสแตนซ์ Java พื้นฐานไปยังเมธอด Java ส่งข้อยกเว้น JavaScript หากประเภท Java ไม่ตรงกับลายเซ็นของพารามิเตอร์ |
ประเภทที่อยู่ในกรอบ (เช่น Integer, Double, Boolean) |
ตัวเลข บูลีน | ไม่รองรับ ระบบจะถือว่าประเภทดั้งเดิมที่อยู่ในกล่องเป็นออบเจ็กต์ทึบแสงและประเมินเป็น null |
| ประเภทดั้งเดิมใดก็ได้ | null / undefined |
บังคับให้ใช้ค่าเริ่มต้น (0, 0.0, \u0000, false) |
Object, String, อาร์เรย์ |
null |
บังคับให้เป็น Java null |
การแมปประเภทการแสดงผล (Java เป็น JavaScript)
เมื่อเมธอด Java หรือ Kotlin ที่มีคำอธิบายประกอบแสดงผลค่า Bridge จะแปลงค่านั้น เป็นประเภท JavaScript
| ประเภทการแสดงผลของ Java | ค่า JavaScript | JavaScript typeof |
|---|---|---|
boolean |
true / false |
"boolean" |
byte, short, int, long, float, double |
หมายเลข | "number" |
char |
ตัวเลข (Code Point ของ Unicode) | "number" |
String (ไม่ใช่ค่าว่าง) |
ค่าสตริง | "string" |
String (null) |
undefined |
"undefined" |
void |
undefined |
"undefined" |
อาร์เรย์ Java (เช่น int[], String[]) |
undefined |
"undefined" ระบบไม่รองรับค่าที่ส่งคืนเป็นอาร์เรย์ ระบบจะไม่เรียกใช้เมธอด Java และจะแสดง undefined โดยไม่แสดงข้อยกเว้น |
| ออบเจ็กต์ Java / ประเภทที่กำหนดเอง (ไม่ใช่ค่าว่าง) | Object Wrapper | "object". สร้าง Wrapper JavaScript รอบอินสแตนซ์ Java โค้ด JavaScript สามารถเรียกใช้เมธอดสาธารณะใดก็ได้ในออบเจ็กต์นี้ที่มีคำอธิบายประกอบ @JavascriptInterface |
ออบเจ็กต์ Java / ประเภทที่กำหนดเอง (null) |
null |
"object" |
Primitive แบบกล่อง (เช่น Integer, Double) |
Object Wrapper | "object" แสดงผลเป็น Wrapper ออบเจ็กต์ Java แบบทึบแสงที่ไม่มีเมธอด @JavascriptInterface ที่เข้าถึงได้ ทำให้ใช้ค่าใน JavaScript ไม่ได้ |
วิธีการและความสามารถในการเข้าถึงของสมาชิก
บริดจ์ JavaScript จะบังคับใช้กฎการเข้าถึงและการมองเห็นสมาชิกอย่างเข้มงวดเพื่อ ป้องกันการเรียกใช้โค้ดโดยไม่ตั้งใจ
- ไม่มีการเปิดเผยฟิลด์: JavaScript เข้าถึงฟิลด์ Java (รวมถึงฟิลด์
publicและpublic final) ไม่ได้ และจะประเมินเป็นundefined - ข้อกำหนดในการใส่คำอธิบายประกอบ: เฉพาะเมธอดที่ใส่คำอธิบายประกอบอย่างชัดเจนด้วย
@JavascriptInterfaceเท่านั้นที่จะแสดงต่อ JavaScript - ข้อจำกัดด้านระดับการเข้าถึง: วิธีการต้องเป็น
publicprivateและprotectedจะไม่แสดงต่อ JavaScript แม้ว่าจะมีคำอธิบายประกอบ@JavascriptInterfaceก็ตาม - เมธอดแบบคงที่: เมธอดแบบคงที่ที่มีคำอธิบายประกอบด้วย
@JavascriptInterfaceจะเรียกใช้จาก JavaScript ได้ - การรับช่วงและการลบล้าง:
@JavascriptInterfaceคำอธิบายประกอบจะไม่ รับช่วงเมื่อคลาสย่อยลบล้างเมธอด หากคลาสย่อยแทนที่เมธอดที่มีคำอธิบายประกอบจากคลาสแม่ คลาสย่อยต้องใส่คำอธิบายประกอบ@JavascriptInterfaceในเมธอดที่แทนที่อย่างชัดเจนเพื่อแสดงต่อ JavaScript เมธอดสาธารณะที่ไม่ได้ลบล้างซึ่งรับค่ามาจากคลาสแม่จะยังคง เข้าถึงได้หากมีการใส่คำอธิบายประกอบในคลาสแม่ - การป้องกันการสะท้อน: ระบบจะบล็อกวิธีการสะท้อน Java มาตรฐาน (เช่น
getClass()) และส่งข้อยกเว้น JavaScript เพื่อป้องกันช่องโหว่การเรียกใช้โค้ดจากระยะไกล - การโอเวอร์โหลดเมธอด: ระบบรองรับเมธอด Java ที่โอเวอร์โหลด Bridge resolves จะเรียกใช้เมธอดตามจำนวนอาร์กิวเมนต์ที่ส่งเท่านั้น และ จะไม่พิจารณาประเภทอาร์กิวเมนต์ การเรียกเมธอดที่โอเวอร์โหลดด้วยจำนวนอาร์กิวเมนต์ที่ไม่ถูกต้องจะทำให้เกิดข้อยกเว้น JavaScript หากฟังก์ชันที่โอเวอร์โหลด 2 รายการมีจำนวนอาร์กิวเมนต์เท่ากัน ระบบจะเลือกฟังก์ชันใดฟังก์ชันหนึ่งโดยพลการ
สรุปกลไก
ตารางต่อไปนี้แสดงการเปรียบเทียบโดยย่อของกลไกการติดตั้งใช้งานบริดจ์เนทีฟหลัก 3 รายการ
| วิธีการ | addWebMessageListener |
postWebMessage |
addJavascriptInterface |
|---|---|---|---|
| การใช้งาน | อะซิงโครนัส (Listener ในชุดข้อความหลัก) | อะซิงโครนัส | พร้อมกัน |
| ความปลอดภัย | สูงสุด (อิงตามรายการที่อนุญาต) | สูง (รับรู้ต้นทาง) | ต่ำ (ไม่มีการตรวจสอบต้นทาง) |
| ความซับซ้อน | ปานกลาง | ปานกลาง | เรียบง่าย |
| ทิศทาง | แบบ 2 ทิศทาง | แบบ 2 ทิศทาง | เว็บไปยังแอป |
| เวอร์ชัน 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) ไปยังและจากแอปที่มาพร้อมเครื่องได้โดยใช้พร็อกซี 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]);
}
};
}
การโหลดข้อมูลขนาดใหญ่อย่างมีประสิทธิภาพ
สำหรับไฟล์ขนาดใหญ่มาก (>10 MB) ให้ใช้วิธี shouldInterceptRequest เพื่อ
สตรีมข้อมูล
- หน้าเว็บจะเริ่ม
fetch()เรียกไปยัง URL ตัวยึดตำแหน่งที่กำหนดเอง เช่น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 ที่ซ่อนอยู่ได้ แต่ก็ไม่สามารถป้องกันช่องโหว่ ของ Cross-Site Scripting (XSS) ภายในโดเมนที่เชื่อถือได้ของคุณเอง เช่น หากหน้าเว็บแสดงเนื้อหาที่ผู้ใช้สร้างขึ้นและมีความเสี่ยงต่อ XSS ที่จัดเก็บไว้ ผู้โจมตีอาจเรียกใช้สคริปต์ที่ทําหน้าที่เป็นต้นทางที่เชื่อถือได้ของคุณ พิจารณา ใช้การตรวจสอบเพย์โหลดของข้อความก่อนที่จะดำเนินการที่ละเอียดอ่อน ในแพลตฟอร์มเนทีฟ
ลดพื้นที่ผิว: แสดงเฉพาะเมธอดหรือข้อมูลที่เฉพาะเจาะจงซึ่งหน้าเว็บต้องการเท่านั้น
ตรวจสอบฟีเจอร์ขณะรันไทม์: Bridge API ล่าสุด ซึ่งรวมถึง
addWebMessageListenerเป็นส่วนหนึ่งของไลบรารี Jetpack Webkit ดังนั้น โปรดตรวจสอบว่ามีบริการสนับสนุนโดยใช้WebViewFeature.isFeatureSupported()ก่อนโทรหา