الوصول إلى واجهات برمجة التطبيقات الأصلية باستخدام JavaScript bridge

تتناول هذه الصفحة الطرق المختلفة وأفضل الممارسات لإنشاء جسر أصلي، يُعرف أيضًا باسم جسر JavaScript، لتسهيل التواصل بين محتوى الويب في WebView وتطبيق Android مضيف.

يتيح ذلك لمطوّري الويب استخدام JavaScript للوصول إلى ميزات النظام الأساسي الأصلية، مثل الكاميرا أو نظام الملفات أو أجهزة الاستشعار المتقدّمة، والتي لا توفّرها عادةً واجهات برمجة التطبيقات العادية على الويب.

حالات الاستخدام

يتيح تنفيذ جسر JavaScript سيناريوهات تكامل مختلفة حيث يتطلّب محتوى الويب وصولاً أعمق إلى نظام التشغيل Android. في ما يلي بعض الأمثلة:

  • عمليات الدمج مع النظام الأساسي: تشغيل مكوّنات واجهة مستخدم Android الأصلية (على سبيل المثال، طلبات المصادقة البيومترية، BottomSheetDialog) من صفحة ويب
  • الأداء: نقل المهام الحسابية المعقّدة إلى رموز Java أو Kotlin الأصلية
  • استمرار البيانات: الوصول إلى قواعد البيانات المشفّرة المحلية أو الإعدادات المفضّلة المشترَكة
  • عمليات نقل البيانات الكبيرة: نقل ملفات وسائط أو بنى بيانات معقّدة بين التطبيق وعارض الويب

آليات التواصل

يوفّر نظام التشغيل Android ثلاثة أجيال أساسية من واجهات برمجة التطبيقات لإنشاء جسر أصلي. وعلى الرغم من أنّها لا تزال متاحة، إلا أنّها تختلف بشكل كبير من حيث الأمان وسهولة الاستخدام والأداء.

استخدام addWebMessageListener (يُنصح به)

addWebMessageListener هي الطريقة الأحدث والأكثر اقتراحًا للتواصل بين محتوى الويب ورمز التطبيق الأصلي. ويجمع بين سهولة استخدام واجهة JavaScript وأمان نظام المراسلة.

طريقة العمل: يضيف التطبيق أداة معالجة تحمل اسمًا معيّنًا ومجموعة من قواعد المصدر المسموح به. بعد ذلك، يتأكّد WebView من أنّ كائن JavaScript متوفّر في النطاق العام (window.objectName) منذ لحظة بدء تحميل الصفحة.

الإعداد: لضمان أنّ WebView تُدرِج عنصر JavaScript قبل تنفيذ أي نص برمجي، عليك استدعاء addWebMessageListener قبل الانتقال إلى الصفحة (مثل استدعاء WebViewCompat.navigate أو loadUrl).

الميزات الأساسية:

  • الأمان والثقة: على عكس واجهات برمجة التطبيقات القديمة، تتطلّب هذه الطريقة Set<String> من allowedOriginRules أثناء عملية الإعداد. وهي الآلية الأساسية لتحديد مستوى الثقة.

    عند تحديد مصدر موثوق به، مثل https://example.com، يضمن WebView أنّه لا يعرض كائنات JavaScript التي تم إدخالها إلا لصفحات الويب التي يتم تحميلها من هذا المصدر تحديدًا.

    تتلقّى دالة رد الاتصال الخاصة بأداة معالجة الأحداث الأصلية المَعلمة 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 التي تم إدخالها من تنفيذ الرموز البرمجية الأصلية.

  • التوافق مع الإطارات المتعددة: تعمل هذه الميزة على مستوى جميع الإطارات التي تتطابق مع قواعد المصدر.

  • إنشاء سلاسل محادثات: يتم تنفيذ معاودة الاتصال الخاصة بمعالج الأحداث في سلسلة المحادثات الرئيسية (واجهة المستخدم) للتطبيق. إذا كان الجسر يحتاج إلى معالجة بيانات معقّدة أو تحليل JSON أو عمليات بحث في قاعدة البيانات، عليك نقل هذه المهام إلى سلسلة محادثات في الخلفية لمنع تجميد واجهة مستخدم التطبيق بسبب الخطأ "التطبيق لا يستجيب" (ANR).

  • ثنائي الاتجاه: عندما تُرسِل صفحة الويب رسالة، يتلقّى التطبيق JavaScriptReplyProxy يمكنه استخدامه لإرسال رسائل إلى هذا الإطار المحدّد. يمكنك الاحتفاظ بهذا العنصر replyProxy واستخدامه في أي وقت لإرسال أي عدد من الرسائل إلى الصفحة، وليس فقط للرد على كل رسالة فردية ترسلها الصفحة. إذا انتقل الإطار الأصلي إلى صفحة أخرى أو تم إتلافه، سيتم تجاهل الرسائل المُرسَلة باستخدام postMessage() على الخادم الوكيل بدون إشعار.

  • بدء العملية من جانب التطبيق: على الرغم من أنّ صفحة الويب يجب أن تبدأ دائمًا قناة التواصل مع التطبيق، يمكن للتطبيق الأصلي أن يطلب من جانب واحد من صفحة الويب بدء هذه العملية. يمكن للتطبيق الأصلي التواصل مع صفحة الويب باستخدام addDocumentStartJavaScript() (لتقييم JavaScript قبل تحميل الصفحة) أو evaluateJavaScript() (لتقييم JavaScript بعد تحميل الصفحة).

قيد: ترسل واجهة برمجة التطبيقات هذه البيانات كسلاسل أو مصفوفات byte[]. بالنسبة إلى بنى البيانات الأكثر تعقيدًا، مثل عناصر JSON، يجب تحويل هذه البنى إلى تسلسل بتنسيق من هذه التنسيقات، ثم إلغاء التسلسل على الجانب الآخر لإعادة إنشاء بنية البيانات.

مثال على الاستخدام:

لفهم التسلسل الكامل لعملية تبادل الرسائل الثنائية الاتجاه، يتم ترتيب الأحداث على النحو التالي:

  1. بدء الاستماع (التطبيق): يسجّل التطبيق الأصلي أداة معالجة الأحداث باستخدام addWebMessageListener ويبدأ عملية التنقل في الصفحة (مثل WebViewCompat.navigate أو loadUrl).
  2. إرسال الرسالة (على الويب): تستدعي JavaScript على صفحة الويب myObject.postMessage(message) لبدء عملية التواصل.
  3. تلقّي الرسائل والردّ عليها (التطبيق): يتلقّى التطبيق الرسالة في دالة معاودة الاتصال الخاصة بالمستمع ويردّ عليها باستخدام replyProxy.postMessage() المقدَّمة.
  4. تلقّي الردّ (على الويب): تتلقّى صفحة الويب الردّ غير المتزامن في دالّة رد الاتصال 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 لإرسال حمولة إلى الإطار الرئيسي لصفحة الويب. لإنشاء قناة اتصال ثنائية الاتجاه، يمكنك إنشاء WebMessageChannel وتمرير أحد منافذه مع الرسالة إلى محتوى الويب.

الخصائص:

  • غير متزامن: على غرار addWebMessageListener، تستخدم هذه الطريقة المراسلة غير المتزامنة، ما يضمن استجابة صفحة الويب لتفاعلات المستخدمين بينما يعالج التطبيق البيانات في الخلفية.
  • إدراك المصدر: يمكنك تحديد targetOrigin لضمان أنّ WebView لا يرسل البيانات إلا إلى موقع إلكتروني موثوق به.

القيود:

  • النطاق: تقتصر هذه الواجهة على التواصل مع الإطار الرئيسي. ولا يتيح توجيه الرسائل أو إرسالها مباشرةً إلى إطارات iframe.
  • قيود معرّف الموارد المنتظم (URI): لا يمكنك استخدام هذه الطريقة للمحتوى الذي يتم تحميله باستخدام معرّفات الموارد المنتظمة data: أو 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 Bridge المستند إلى 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 رقم (عدد صحيح) تم تحويلها إلى قيمة موضع رمز Unicode المقابلة.
char عدد غير صحيح، NaN، Infinity يتم تحويلها إلى \u0000.
boolean true / false يتم تحويلها إلى true أو false في Java.
boolean رقم، سلسلة، عنصر يتم تحويلها إلى false (بما في ذلك السلاسل غير الفارغة والأرقام غير الصفرية).
String سلسلة يتم الاحتفاظ بقيمة السلسلة.
String رقم أو قيمة منطقية يتم تنسيقه كتمثيل سلسلة (على سبيل المثال، "42" أو "true" أو "false").
String null / undefined يتم تحويل null إلى null في Java، ويتم تحويل undefined إلى السلسلة الحرفية "undefined".
String Object وArrayBuffer وTypedArray يتم تحويلها إلى السلسلة الحرفية "undefined".
صفيف أساسي (مثل int[] أو byte[] أو boolean[]) أو String[] مصفوفة ([...]) تحويل إلى مصفوفة Java أحادية الأبعاد من نوع العنصر المستهدَف تعبئ المصفوفات المتفرقة الفهارس غير المعيّنة بقيم تلقائية (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 أو فئة مخصّصة برنامج تضمين كائن 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 مشروحة قيمة، يحوّلها الجسر إلى نوع JavaScript:

نوع القيمة التي يتم إرجاعها في Java قيمة JavaScript ‫JavaScript typeof
boolean true / false "boolean"
byte، short، int، long، float، double الرقم "number"
char رقم (قيمة موضع رمز Unicode) "number"
String (غير فارغ) قيمة السلسلة "string"
String ‎(null) undefined "undefined"
void undefined "undefined"
مصفوفة Java (مثل int[] أو String[]) undefined "undefined". لا تتوفّر إمكانية عرض قيم الصفيف. لا يتم تنفيذ طريقة Java، ويتم عرض undefined بدون طرح استثناء.
عنصر Java / نوع مخصّص (غير فارغ) برنامج تضمين العنصر "object": لإنشاء برنامج تضمين JavaScript حول مثيل Java. يمكن لرمز JavaScript البرمجي طلب أي طريقة عامة في هذا العنصر تمّت إضافة التعليق التوضيحي @JavascriptInterface إليها.
عنصر Java / نوع مخصّص (null) null "object"
نوع أساسي محاط بمربع (مثل Integer أو Double) برنامج تضمين العنصر "object": يتم عرضها كبرنامج تضمين غير شفاف لكائن Java بدون طرق @JavascriptInterface يمكن الوصول إليها، ما يجعل القيمة غير قابلة للاستخدام في JavaScript.

طريقة الوصول إلى العضوية

يفرض جسر JavaScript قواعد صارمة بشأن الوصول إلى الأعضاء ومستوى الظهور للحماية من تنفيذ الرموز البرمجية غير المقصود:

  • الحقول غير مكشوفة: لا يمكن الوصول إلى حقول Java (بما في ذلك الحقلان public وpublic final) من JavaScript، ويتم تقييمها على أنّها undefined.
  • متطلبات التعليقات التوضيحية: لا يتم عرض سوى الطرق التي تمّت إضافة تعليقات توضيحية إليها بشكل صريح باستخدام @JavascriptInterface في JavaScript.
  • قيود الظهور: يجب أن تكون الطرق public. لا يتم عرض الطريقتَين private وprotected مطلقًا في JavaScript، حتى إذا كانتا تحملان التعليق التوضيحي @JavascriptInterface.
  • الطُرق الثابتة: يمكن استدعاء الطُرق الثابتة التي تمّت إضافة التعليقات التوضيحية إليها باستخدام @JavascriptInterface من JavaScript.
  • الوراثة والتجاوز: لا يتم توريث تعليقات @JavascriptInterface التوضيحية عندما تتجاوز فئة فرعية إحدى الطرق. إذا كانت فئة فرعية تتجاوز طريقة موضّحة من فئة رئيسية، يجب أن تتضمّن الفئة الفرعية بشكل صريح التعليق التوضيحي @JavascriptInterface على الطريقة المتجاوزة لعرضها على JavaScript. تظل الطرق العامة غير المعدَّلة الموروثة من فئة رئيسية متاحة إذا تم وضع تعليق توضيحي عليها في الفئة الرئيسية.
  • الحماية من الانعكاس: يتم حظر طرق انعكاس Java العادية (مثل getClass()) ويتم طرح استثناء JavaScript لمنع ثغرات تنفيذ الرموز البرمجية عن بُعد.
  • تحميل الأساليب الزائد: تتوافق هذه الميزة مع أساليب Java المحمّلة بشكل زائد. تعمل طريقة bridge resolves على حل استدعاءات الطرق استنادًا إلى عدد الوسيطات التي تم تمريرها فقط، ولا تأخذ أنواع الوسيطات في الاعتبار. يؤدي استدعاء طريقة محمّلة بشكل زائد مع عدد وسيطات غير صالح إلى حدوث استثناء في JavaScript. إذا كان هناك حمولتان زائدتان لهما عدد الوسيطات نفسه، سيتم اختيار إحداهما بشكل عشوائي.

ملخّص الآليات

يقدّم الجدول التالي مقارنة سريعة لآليات التنفيذ الأساسية الثلاث لجسر التطبيق الأصلي:

الطريقة addWebMessageListener postWebMessage addJavascriptInterface
التنفيذ غير متزامن (أداة المعالجة في سلسلة التعليمات الرئيسية) بدون تزامن متزامن
الأمان الأعلى (استنادًا إلى القائمة المسموح بها) عالية (تتضمّن معلومات المصدر) منخفضة (بدون عمليات تحقّق من المصدر)
التعقيد متوسط متوسط بسيط
الاتجاه ثنائي الاتجاه ثنائي الاتجاه من الويب إلى التطبيق
الحدّ الأدنى لإصدار 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 ميغابايت)، استخدِم طريقة 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 المخفية، لا يمكنها الحماية من ثغرات النصوص البرمجية للمواقع المتقاطعة (XSS) ضمن نطاقك الموثوق به. على سبيل المثال، إذا كانت صفحة الويب تعرض محتوًى من إنشاء المستخدمين وكانت عرضة لهجمات البرمجة عبر المواقع المخزّنة، يمكن للمهاجم تنفيذ برنامج نصي يعمل كمصدر موثوق به. ننصحك بتطبيق عملية التحقّق من صحة البيانات على حمولات الرسائل قبل تنفيذ عمليات حساسة على النظام الأساسي.

  • تقليل مساحة العرض: لا تعرض سوى الطرق أو البيانات المحدّدة التي تتطلّبها صفحة الويب.

  • التحقّق من الميزات في وقت التشغيل: تشكّل واجهات برمجة التطبيقات الحديثة الخاصة بالجسر، بما في ذلك addWebMessageListener، جزءًا من مكتبة Jetpack Webkit. لذلك، عليك دائمًا التحقّق من توفّر الدعم باستخدام WebViewFeature.isFeatureSupported() قبل الاتصال بهم.