เข้าถึง API ดั้งเดิมด้วย JavaScript Bridge

หน้านี้จะกล่าวถึงวิธีการต่างๆ และแนวทางปฏิบัติแนะนำในการสร้าง บริดจ์เนทีฟ หรือที่เรียกว่าบริดจ์ 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.com WebView จะรับประกันว่าระบบจะแสดงออบเจ็กต์ 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 ทาง เหตุการณ์ จะดำเนินการตามลำดับต่อไปนี้

  1. การเริ่มต้น (แอป): แอปที่มาพร้อมเครื่องจะลงทะเบียน Listener ด้วย addWebMessageListener และเริ่มการนำทางหน้าเว็บ (เช่น ด้วย WebViewCompat.navigate หรือ loadUrl)
  2. ส่งข้อความ (เว็บ): การเรียก JavaScript ของหน้าเว็บ myObject.postMessage(message) เพื่อเริ่มการสื่อสาร
  3. รับและตอบกลับข้อความ (แอป): แอปจะรับข้อความใน การเรียกกลับของ Listener และตอบกลับโดยใช้ replyProxy.postMessage() ที่ระบุ
  4. รับการตอบกลับ (เว็บ): หน้าเว็บจะได้รับการตอบกลับแบบอะซิงโครนัสในฟังก์ชัน 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
  • ข้อจำกัดด้านระดับการเข้าถึง: วิธีการต้องเป็น public private และ 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 เพื่อ สตรีมข้อมูล

  1. หน้าเว็บจะเริ่มfetch()เรียกไปยัง URL ตัวยึดตำแหน่งที่กำหนดเอง เช่น https://app.local/large-file
  2. แอป Android จะสกัดกั้นคำขอนี้ใน WebViewClient.shouldInterceptRequest
  3. แอปจะแสดงข้อมูลเป็น 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() ก่อนโทรหา