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

หน้านี้จะกล่าวถึงวิธีการต่างๆ และแนวทางปฏิบัติแนะนำสำหรับการสร้าง Native Bridge หรือที่เรียกว่า JavaScript Bridge เพื่ออำนวยความสะดวกในการสื่อสารระหว่างเนื้อหาเว็บใน WebView กับแอปพลิเคชัน Android โฮสต์

ซึ่งจะช่วยให้นักพัฒนาเว็บใช้ JavaScript เพื่อเข้าถึงฟีเจอร์แพลตฟอร์มแบบเนทีฟ เช่น กล้อง ระบบไฟล์ หรือเซ็นเซอร์ฮาร์ดแวร์ขั้นสูง ซึ่งโดยปกติแล้ว API เว็บมาตรฐานจะไม่มีให้

กรณีการใช้งาน

การใช้งาน JavaScript Bridge ช่วยให้เกิดสถานการณ์การผสานรวมต่างๆ ที่เนื้อหาเว็บต้องเข้าถึงระบบปฏิบัติการ Android ในระดับที่ลึกขึ้น ตัวอย่างเช่น

  • การผสานรวมแพลตฟอร์ม: การทริกเกอร์คอมโพเนนต์ UI แบบเนทีฟของ Android (เช่น พรอมต์ไบโอเมตริก BottomSheetDialog) จากหน้าเว็บ
  • ประสิทธิภาพ: การโอนงานการคำนวณที่ซับซ้อนไปยังโค้ด Java หรือ Kotlin แบบเนทีฟ
  • การคงอยู่ของข้อมูล: การเข้าถึงฐานข้อมูลที่เข้ารหัสไว้ในเครื่องหรือค่ากำหนดที่แชร์
  • การโอนข้อมูลขนาดใหญ่: การส่งไฟล์สื่อหรือโครงสร้างข้อมูลที่ซับซ้อน ระหว่างแอปกับตัวแสดงผลเว็บ

กลไกการสื่อสาร

Android มี API 3 รุ่นหลักๆ สำหรับสร้าง Native Bridge แม้ว่า API ทั้งหมดจะยังคงใช้งานได้ แต่ก็มีความแตกต่างกันอย่างมากในด้านความปลอดภัย ความสามารถในการใช้งาน และประสิทธิภาพ

ใช้ addWebMessageListener (แนะนำ)

addWebMessageListener เป็นแนวทางที่ทันสมัยที่สุดและแนะนำให้ใช้สำหรับการสื่อสารระหว่างเนื้อหาเว็บกับโค้ดแอปที่มาพร้อมเครื่อง โดยจะรวมความสะดวกในการใช้งานของอินเทอร์เฟซ JavaScript เข้ากับความปลอดภัยของระบบการรับส่งข้อความ

วิธีการทำงาน: แอปจะเพิ่ม Listener ที่มีชื่อเฉพาะและชุดกฎต้นทางที่ อนุญาต จากนั้น WebView จะตรวจสอบว่าออบเจ็กต์ JavaScript อยู่ในขอบเขตส่วนกลาง (window.objectName) ตั้งแต่เริ่มโหลดหน้าเว็บ

การเริ่มต้น: หากต้องการให้ WebView แทรกออบเจ็กต์ JavaScript ก่อน ที่สคริปต์จะทำงาน คุณต้องเรียกใช้ addWebMessageListener ก่อนเรียกใช้ loadUrl()

ฟีเจอร์หลัก:

  • ความปลอดภัยและความน่าเชื่อถือ: วิธีนี้กำหนดให้มี Set<String> ของ allowedOriginRules ในระหว่างการเริ่มต้น ซึ่งแตกต่างจาก API เดิม และเป็นกลไกหลักในการสร้างความน่าเชื่อถือ

    เมื่อคุณระบุต้นทางที่เชื่อถือได้ เช่น https://example.com WebView จะรับประกันว่าจะแสดงออบเจ็กต์ JavaScript ที่แทรกไว้เฉพาะกับหน้าเว็บที่โหลดจากต้นทางนั้นๆ เท่านั้น

    Callback ของ Listener แบบเนทีฟจะได้รับพารามิเตอร์ sourceOrigin พร้อมกับทุกข้อความ คุณสามารถใช้พารามิเตอร์นี้เพื่อยืนยันต้นทางที่แน่นอนของผู้ส่งได้หาก Bridge รองรับต้นทางที่อนุญาตหลายรายการ

    เนื่องจาก 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") คุณไม่สามารถใช้ไวลด์การ์ดสำหรับรูปแบบหรือตรงกลางโดเมน

    ซึ่งจะจำกัด Bridge ไว้ที่โดเมนที่ยืนยันแล้ว เพื่อป้องกันไม่ให้เนื้อหาของบุคคลที่สามที่ไม่ได้รับอนุญาตหรือ iframe ที่แทรกไว้เรียกใช้โค้ดแบบเนทีฟ

  • การรองรับหลายเฟรม: ทำงานในทุกเฟรมที่ตรงกับกฎต้นทาง

  • การทำงานแบบเธรด: Callback ของ Listener จะทำงานในเธรดหลัก (UI) ของแอปพลิเคชัน หาก Bridge ต้องจัดการการประมวลผลข้อมูลที่ซับซ้อน การแยกวิเคราะห์ JSON หรือการค้นหาฐานข้อมูล คุณต้องลดภาระงานดังกล่าวไปยังเทรดเบื้องหลังเพื่อป้องกันไม่ให้ UI ของแอปพลิเคชันหยุดทำงานเนื่องจากข้อผิดพลาด "แอปพลิเคชันไม่ตอบสนอง" (ANR)

  • แบบ 2 ทิศทาง: เมื่อหน้าเว็บส่งข้อความ แอปจะได้รับ JavaScriptReplyProxy ซึ่งใช้ส่งข้อความกลับไปยัง เฟรมนั้นๆ ได้ คุณสามารถเก็บออบเจ็กต์ replyProxy นี้ไว้และใช้ได้ทุกเมื่อเพื่อส่งข้อความจำนวนเท่าใดก็ได้ไปยังหน้าเว็บ ไม่ใช่แค่การตอบกลับข้อความแต่ละรายการที่หน้าเว็บส่งมา หากเฟรมต้นทางออกจากหน้าเว็บหรือถูกทำลาย ระบบจะละเว้นข้อความที่ส่งโดยใช้ postMessage() ในพร็อกซีโดยไม่มีการแจ้งเตือน

  • การเริ่มต้นจากฝั่งแอป: แม้ว่าหน้าเว็บจะต้องเริ่มต้นช่องทางการสื่อสารกับแอปเสมอ แต่แอปที่มาพร้อมเครื่องก็สามารถแจ้งให้หน้าเว็บเริ่มกระบวนการนี้ได้โดยฝ่ายเดียว แอปที่มาพร้อมเครื่องสามารถสื่อสารกับหน้าเว็บได้ด้วย addDocumentStartJavaScript() (เพื่อประเมิน JavaScript ก่อนที่หน้าเว็บจะโหลด) หรือ evaluateJavaScript() (เพื่อประเมิน JavaScript หลังจากที่หน้าเว็บโหลดแล้ว)

ข้อจำกัด: API นี้จะส่งข้อมูลเป็นสตริงหรืออาร์เรย์ byte[] สำหรับโครงสร้างข้อมูลที่ซับซ้อนมากขึ้น เช่น ออบเจ็กต์ JSON คุณต้องซีเรียลไลซ์ข้อมูลนี้เป็นรูปแบบใดรูปแบบหนึ่ง แล้วจึงดีซีเรียลไลซ์ในอีกด้านหนึ่งเพื่อสร้างโครงสร้างข้อมูลใหม่

ตัวอย่างการใช้งาน:

หากต้องการทำความเข้าใจลำดับทั้งหมดของการแลกเปลี่ยนข้อความแบบ 2 ทิศทาง เหตุการณ์จะเกิดขึ้นตามลำดับต่อไปนี้

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

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: คุณไม่สามารถใช้วิธีนี้กับเนื้อหาที่โหลดโดยใช้ data: URI, file: URI หรือ loadData() เว้นแต่จะระบุ "*" เป็น ต้นทางเป้าหมาย การดำเนินการนี้จะทำให้ทุกหน้าเว็บได้รับข้อความ
  • ความเสี่ยงด้านข้อมูลประจำตัว: เนื้อหาเว็บไม่มีวิธีที่ชัดเจนในการยืนยัน ข้อมูลประจำตัวของผู้ส่ง ข้อความที่หน้าเว็บได้รับอาจมาจากแอปที่มาพร้อมเครื่องหรือ iframe อื่น

ใช้วิธีนี้เมื่อคุณต้องการช่องทางแบบไม่พร้อมกันที่เรียบง่ายสำหรับข้อมูลที่อิงตามสตริงใน Android เวอร์ชันก่อนหน้าซึ่งไม่รองรับ addWebMessageListener

ใช้ addJavascriptInterface (เดิม)

วิธีที่เก่าที่สุดคือการแทรกอินสแตนซ์ออบเจ็กต์แบบเนทีฟลงใน WebView โดยตรง

วิธีการทำงาน: คุณกำหนดคลาส Kotlin หรือ Java ใส่คำอธิบายประกอบในเมธอดที่อนุญาตด้วย @JavascriptInterface และเพิ่มอินสแตนซ์ของคลาสลงใน WebView โดยใช้ addJavascriptInterface(Object, String)

ลักษณะ:

  • พร้อมกัน: สภาพแวดล้อมการดำเนินการ JavaScript จะถูกบล็อกจนกว่า เมธอดในโค้ด Android จะแสดงผล
  • ความปลอดภัยของเธรด: ระบบจะเรียกใช้เมธอดในเธรดเบื้องหลัง ซึ่งต้องมีการซิงโครไนซ์อย่างระมัดระวังในฝั่ง Kotlin หรือ Java
  • ความเสี่ยงด้านความปลอดภัย: โดยค่าเริ่มต้น addJavascriptInterface จะพร้อมใช้งานสำหรับ ทุกเฟรมภายใน WebView รวมถึง iframe และไม่มีการควบคุมการเข้าถึงตามต้นทาง เนื่องจากลักษณะการทำงานแบบอะซิงโครนัสของ WebView จึงไม่สามารถกำหนด URL ของเฟรมที่เรียกใช้อินเทอร์เฟซของคุณได้อย่างปลอดภัย คุณต้องไม่พึ่งพาเมธอดอย่าง WebView.getUrl() สำหรับการยืนยันความปลอดภัย เนื่องจากเมธอดเหล่านี้ไม่รับประกันความถูกต้องและไม่ได้ระบุว่าเฟรมใดเป็นผู้ส่งคำขอ

สรุปกลไก

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

ซึ่งจะช่วยให้สตรีมข้อมูลเป็นชิ้นๆ แทนที่จะโหลดเพย์โหลดทั้งหมดลงในหน่วยความจำพร้อมกัน

ฟังก์ชัน JavaScript ต่อไปนี้แสดงโค้ดฝั่งไคลเอ็นต์สำหรับการโหลดไฟล์ไบนารีขนาดใหญ่จากแอปพลิเคชันแบบเนทีฟอย่างมีประสิทธิภาพโดยใช้การเรียก fetch() มาตรฐานไปยัง URL ที่กำหนดเองซึ่งเป็น 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);
  }
});

ปฏิบัติตามคำแนะนำด้านความปลอดภัย

ปฏิบัติตามหลักเกณฑ์ต่อไปนี้เมื่อใช้งาน Bridge เพื่อปกป้องแอปพลิเคชันและข้อมูลผู้ใช้

  • บังคับใช้ HTTPS: อนุญาตให้สื่อสารกับต้นทางที่ปลอดภัยเท่านั้น เพื่อให้มั่นใจว่าเนื้อหาของบุคคลที่สามที่เป็นอันตรายจะไม่สามารถ เรียกใช้ตรรกะแบบเนทีฟของแอปพลิเคชันได้

  • ใช้กฎต้นทาง: วิธีที่ดีที่สุดในการจัดการความน่าเชื่อถือคือการ กำหนด allowedOriginRules อย่างเข้มงวดและตรวจสอบ sourceOrigin ที่ระบุไว้ใน Callback ของข้อความ หลีกเลี่ยงการใช้ไวลด์การ์ดแบบเต็ม (*) ซึ่งตรงกับต้นทางทั้งหมดเป็นกฎต้นทางเดียว เว้นแต่จะจำเป็นจริงๆ การใช้ไวลด์การ์ดสำหรับโดเมนย่อย (เช่น *.example.com) ยังคงใช้ได้และปลอดภัยสำหรับการจับคู่โดเมนย่อยหลายรายการ (เช่น foo.example.com, bar.example.com)

    หมายเหตุ: แม้ว่ากฎต้นทางจะช่วยป้องกันเว็บไซต์ของบุคคลที่สามที่เป็นอันตราย และ iframe ที่ซ่อนอยู่ แต่ก็ไม่สามารถป้องกันช่องโหว่ Cross-site Scripting (XSS) ภายในโดเมนที่เชื่อถือได้ของคุณเอง เช่น หากหน้าเว็บแสดงเนื้อหาที่ผู้ใช้สร้างขึ้นและมีช่องโหว่ XSS ที่จัดเก็บไว้ ผู้โจมตีอาจเรียกใช้สคริปต์ที่ทำหน้าที่เป็นต้นทางที่เชื่อถือได้ของคุณ พิจารณาใช้การตรวจสอบความถูกต้องกับเพย์โหลดของข้อความก่อนที่จะดำเนินการแพลตฟอร์มแบบเนทีฟที่ละเอียดอ่อน

  • ลดพื้นที่ผิว: แสดงเฉพาะเมธอดหรือข้อมูลที่ หน้าเว็บต้องการ

  • ตรวจสอบฟีเจอร์ขณะรันไทม์: API ของ Bridge ล่าสุด รวมถึง addWebMessageListener เป็นส่วนหนึ่งของไลบรารี Jetpack Webkit ดังนั้น ให้ตรวจสอบการรองรับโดยใช้ WebViewFeature.isFeatureSupported() เสมอก่อนที่จะเรียกใช้