دسترسی به API های بومی با پل جاوا اسکریپت

این صفحه به بررسی روش‌ها و بهترین شیوه‌های مختلف برای ایجاد یک پل بومی، که با نام پل جاوا اسکریپت نیز شناخته می‌شود، می‌پردازد تا ارتباط بین محتوای وب در یک WebView و یک برنامه اندروید میزبان را تسهیل کند.

این امر توسعه‌دهندگان وب را قادر می‌سازد تا از جاوا اسکریپت برای دسترسی به ویژگی‌های بومی پلتفرم - مانند دوربین، سیستم فایل یا حسگرهای سخت‌افزاری پیشرفته - که APIهای استاندارد وب معمولاً ارائه نمی‌دهند، استفاده کنند.

موارد استفاده

پیاده‌سازی پل جاوااسکریپت، سناریوهای مختلف ادغام را که در آن‌ها محتوای وب نیاز به دسترسی عمیق‌تر به سیستم عامل اندروید دارد، امکان‌پذیر می‌کند. در زیر چند نمونه آورده شده است:

  • یکپارچه‌سازی پلتفرم : فعال‌سازی اجزای رابط کاربری بومی اندروید (برای مثال، Biometric prompts، BottomSheetDialog ) از یک صفحه وب.
  • عملکرد : واگذاری وظایف محاسباتی سنگین به کد بومی جاوا یا کاتلین.
  • ماندگاری داده‌ها : دسترسی به پایگاه‌های داده رمزگذاری شده محلی یا تنظیمات برگزیده مشترک.
  • انتقال داده‌های حجیم : انتقال فایل‌های رسانه‌ای یا ساختارهای داده پیچیده بین برنامه و رندرکننده وب.

مکانیسم‌های ارتباطی

اندروید سه نسل اصلی از APIها را برای ایجاد یک پل بومی ارائه می‌دهد. اگرچه همه آنها هنوز در دسترس هستند، اما از نظر امنیت، قابلیت استفاده و عملکرد تفاوت‌های چشمگیری دارند.

استفاده از addWebMessageListener (توصیه می‌شود)

addWebMessageListener مدرن‌ترین و توصیه‌شده‌ترین رویکرد برای ارتباط بین محتوای وب و کد برنامه بومی است. این رویکرد، سهولت استفاده از رابط جاوا اسکریپت را با امنیت سیستم پیام‌رسانی ترکیب می‌کند.

نحوه کار : برنامه یک شنونده با نام خاص و مجموعه‌ای از قوانین مبدا مجاز اضافه می‌کند. سپس WebView از لحظه شروع بارگذاری صفحه، اطمینان حاصل می‌کند که شیء جاوا اسکریپت در محدوده سراسری ( window.objectName ) وجود دارد.

مقداردهی اولیه : برای اطمینان از اینکه WebView قبل از اجرای هر اسکریپتی، شیء جاوا اسکریپت را تزریق می‌کند، باید قبل از پیمایش به صفحه (مانند فراخوانی WebViewCompat.navigate یا loadUrladdWebMessageListener فراخوانی کنید.

ویژگی‌های کلیدی :

  • امنیت و اعتماد : برخلاف APIهای قدیمی، این روش در طول مقداردهی اولیه به Set<String> از allowedOriginRules نیاز دارد. این مکانیسم اصلی برای ایجاد اعتماد است.

    وقتی یک منبع معتبر مانند https://example.com را مشخص می‌کنید، WebView تضمین می‌کند که اشیاء جاوا اسکریپت تزریق شده را فقط در صفحات وب بارگذاری شده از همان منبع دقیق نمایش دهد.

    تابع فراخوانی شنونده نیتیو (native listener) با هر پیام، یک پارامتر sourceOrigin دریافت می‌کند. اگر بریج شما از چندین مبدا مجاز پشتیبانی می‌کند، می‌توانید از این برای تأیید مبدا دقیق فرستنده استفاده کنید.

    از آنجا که وب‌ویو این بررسی‌های مبدا را در سطح پلتفرم به شدت اجرا می‌کند، برنامه شما می‌تواند به طور کلی به پیام‌های دریافتی از یک sourceOrigin معتبر به عنوان پیام‌های معتبر تکیه کند و نیاز به اعتبارسنجی دقیق بار داده را در اکثر پیاده‌سازی‌های استاندارد از بین ببرد.

    • وب ویو قوانین را با طرح (HTTP/HTTPS)، میزبان و پورت مطابقت می‌دهد.
    • وب ویو مسیرها را نادیده می‌گیرد. برای مثال، https://example.com https://example.com/login و https://example.com/home اجازه می‌دهد.
    • وب‌ویو به شدت استفاده از wildcardها را برای زیردامنه‌ها به ابتدای میزبان محدود می‌کند. برای مثال، https://*.example.com با https://foo.example.com مطابقت دارد اما با https://example.com مطابقت ندارد. اگر نیاز دارید که هم https://example.com و هم زیردامنه‌های آن را مطابقت دهید، باید هر قانون مبدا را جداگانه به لیست مجاز اضافه کنید (برای مثال، "https://example.com", "https://*.example.com" ). شما نمی‌توانید از wildcardها برای طرح یا در وسط یک دامنه استفاده کنید.

    این کار، پل را به دامنه‌های تأیید شده محدود می‌کند و از اجرای کد بومی توسط محتوای غیرمجاز شخص ثالث یا iframeهای تزریق‌شده جلوگیری می‌کند.

  • پشتیبانی از چند فریم : در تمام فریم‌هایی که با قوانین مبدا مطابقت دارند، کار می‌کند.

  • Threading : فراخوانی شنونده (listener callback) روی thread اصلی (UI) برنامه اجرا می‌شود. اگر bridge شما نیاز به پردازش داده‌های پیچیده، تجزیه JSON یا جستجو در پایگاه داده دارد، باید این کار را به یک thread پس‌زمینه منتقل کنید تا از هنگ کردن UI برنامه با خطای "app not responding" (ANR) جلوگیری شود.

  • دوطرفه : وقتی صفحه وب پیامی ارسال می‌کند، برنامه یک JavaScriptReplyProxy دریافت می‌کند که می‌تواند از آن برای ارسال پیام‌ها به آن فریم خاص استفاده کند. شما می‌توانید این شیء replyProxy را نگه دارید و در هر زمانی از آن برای ارسال هر تعداد پیام به صفحه استفاده کنید، نه فقط برای پاسخ به هر پیام جداگانه‌ای که صفحه ارسال می‌کند. اگر فریم مبدا حرکت نکند یا از بین برود، پیام‌های ارسالی با استفاده از postMessage() روی پروکسی بی‌صدا نادیده گرفته می‌شوند.

  • شروع به کار در سمت برنامه : اگرچه صفحه وب همیشه باید کانال ارتباطی با برنامه را آغاز کند، برنامه بومی می‌تواند به طور یکجانبه صفحه وب را برای شروع این فرآیند ترغیب کند. برنامه بومی می‌تواند با استفاده از addDocumentStartJavaScript() (برای ارزیابی جاوا اسکریپت قبل از بارگذاری صفحه) یا evaluateJavaScript() (برای ارزیابی جاوا اسکریپت پس از بارگذاری صفحه) با صفحه وب ارتباط برقرار کند.

محدودیت : این API داده‌ها را به صورت رشته یا آرایه‌های byte[] ارسال می‌کند. برای ساختارهای داده پیچیده‌تر، مانند اشیاء JSON، باید این را به یکی از این قالب‌ها سریالایز کنید و سپس در طرف دیگر deserialize کنید تا ساختار داده بازسازی شود.

مثال کاربرد :

برای درک کامل توالی تبادل پیام دو طرفه، رویدادها به این ترتیب ادامه می‌یابند:

  1. شروع (برنامه) : برنامه بومی شنونده را با addWebMessageListener ثبت می‌کند و پیمایش صفحه را آغاز می‌کند (مانند WebViewCompat.navigate یا loadUrl ).
  2. ارسال پیام (وب) : جاوا اسکریپت صفحه وب، myObject.postMessage(message) را برای شروع ارتباط فراخوانی می‌کند.
  3. دریافت و پاسخ پیام (برنامه) : برنامه پیام را در callback شنونده دریافت می‌کند و با استفاده از replyProxy.postMessage() ارائه شده پاسخ می‌دهد.
  4. دریافت پاسخ (وب) : صفحه وب پاسخ ناهمزمان را در تابع فراخوانی myObject.onmessage() دریافت می‌کند.

کاتلین

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)
}

جاوا

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);
}

کد جاوا اسکریپت زیر پیاده‌سازی سمت کلاینت addWebMessageListener را نشان می‌دهد که به محتوای وب اجازه می‌دهد پیام‌ها را از برنامه بومی دریافت کند و پیام‌های خود را از طریق پروکسی myObject ارسال کند.

myObject.onmessage = function(event) {
    console.log("App says: " + event.data);
};
myObject.postMessage("Hello world!");

استفاده از postWebMessage (جایگزین)

اندروید این را معرفی کرد تا یک جایگزین مبتنی بر پیام‌رسانی ناهمزمان، مشابه window.postMessage وب، ارائه دهد.

نحوه کار : این برنامه از WebViewCompat.postWebMessage برای ارسال یک payload به فریم اصلی صفحه وب استفاده می‌کند. برای ایجاد یک کانال ارتباطی دو طرفه، می‌توانید یک WebMessageChannel ایجاد کنید و یکی از پورت‌های آن را به همراه پیام به محتوای وب منتقل کنید.

ویژگی‌ها :

  • ناهمگام (Asynchronous ): مانند addWebMessageListener ، این متد از پیام‌رسانی ناهمگام استفاده می‌کند که تضمین می‌کند صفحه وب در حالی که برنامه داده‌ها را در پس‌زمینه پردازش می‌کند، به تعاملات کاربر پاسخگو باقی می‌ماند.
  • آگاه از مبدا : می‌توانید یک targetOrigin مشخص کنید تا مطمئن شوید که WebView داده‌ها را فقط به یک وب‌سایت معتبر ارسال می‌کند.

محدودیت‌ها :

  • محدوده : این API ارتباط را به فریم اصلی محدود می‌کند. از آدرس‌دهی مستقیم یا ارسال پیام به iframeها پشتیبانی نمی‌کند.
  • محدودیت‌های URI : شما نمی‌توانید از این روش برای محتوایی که با استفاده data: URIs، file: URIs یا loadData() بارگذاری می‌شود استفاده کنید، مگر اینکه "*" را به عنوان مبدا هدف مشخص کنید. انجام این کار به هر صفحه‌ای اجازه می‌دهد پیام را دریافت کند.
  • خطر هویت : هیچ راه مشخصی برای تأیید هویت فرستنده توسط محتوای وب وجود ندارد. پیامی که صفحه وب دریافت می‌کند می‌تواند از برنامه اصلی شما یا یک iframe دیگر باشد.

از این متد زمانی استفاده کنید که به یک کانال ساده و غیرهمزمان برای داده‌های مبتنی بر رشته در نسخه‌های قدیمی‌تر اندروید که addWebMessageListener پشتیبانی نمی‌کنند، نیاز دارید.

استفاده از addJavascriptInterface (قدیمی)

قدیمی‌ترین روش شامل تزریق مستقیم یک نمونه شیء بومی به WebView است.

نحوه کار : شما یک کلاس Kotlin یا Java تعریف می‌کنید، متدهای مجاز را با @JavascriptInterface حاشیه‌نویسی می‌کنید و با استفاده از addJavascriptInterface(Object, String) یک نمونه از کلاس را به WebView اضافه می‌کنید.

ویژگی‌ها :

  • همگام (Synchronous ): محیط اجرای جاوا اسکریپت تا زمانی که متد موجود در کد اندروید شما خروجی دهد، مسدود می‌شود.
  • ایمنی نخ : سیستم متدها را روی یک نخ پس‌زمینه فراخوانی می‌کند که نیاز به همگام‌سازی دقیق در سمت کاتلین یا جاوا دارد.
  • ریسک امنیتی : به طور پیش‌فرض، addJavascriptInterface برای هر فریم درون WebView، از جمله iframeها، در دسترس است. فاقد کنترل دسترسی مبتنی بر مبدا است. به دلیل رفتار ناهمزمان WebView، تعیین ایمن URL فریمی که رابط شما را فراخوانی می‌کند، امکان‌پذیر نیست. شما نباید برای تأیید امنیت به روش‌هایی مانند WebView.getUrl() تکیه کنید، زیرا تضمینی برای دقت آنها وجود ندارد و مشخص نمی‌کنند که کدام فریم خاص درخواست را انجام داده است.

تبدیل نوع داده‌ها و اجبار

هنگام استفاده از addJavascriptInterface ، پل جاوا مبتنی بر Chromium انواع داده‌ها را بین زمان اجرای جاوا اسکریپت و کد برنامه اندروید شما تبدیل می‌کند.

قوانین اجباری زیر برای پارامترهای متد و مقادیر برگشتی اعمال می‌شوند.

نگاشت نوع پارامتر (از جاوا اسکریپت به جاوا)

وقتی جاوا اسکریپت آرگومان‌ها را به یک متد حاشیه‌نویسی شده جاوا یا کاتلین ارسال می‌کند، پل، مقادیر جاوا اسکریپت را به انواع پارامترهای جاوای مربوطه تبدیل می‌کند:

نوع پارامتر جاوا مقدار آرگومان جاوا اسکریپت رفتار اجباری
byte ، short ، int ، long عدد (عدد صحیح) مقادیر به نوع عدد صحیح هدف تبدیل می‌شوند. مقادیر خارج از محدوده طبق قوانین استاندارد تبدیل عددی، به اطراف منتقل می‌شوند.
byte ، short ، int ، long NaN به 0 وادار می‌کند.
byte ، short ، int ، long Infinity برای byte و short به -1 و برای int و long به Integer.MAX_VALUE و Long.MAX_VALUE اجبار می‌کند.
float ، double شماره به مقدار ممیز شناور متناظر جاوا تبدیل می‌کند.
float ، double NaN / Infinity تابع ()Coerces را به Float.NaN ، Double.NaN ، Float.POSITIVE_INFINITY یا Double.POSITIVE_INFINITY وادار می‌کند.
char عدد (عدد صحیح) به کد پوینت یونیکد مربوطه تبدیل شد.
char غیر صحیح، NaN ، Infinity وادار به \u0000 می‌کند.
boolean true / false جاوا را مجبور به true یا false می‌کند.
boolean عدد، رشته، شیء اجباراً مقدار را false برمی‌گرداند (شامل رشته‌های غیر تهی و اعداد غیر صفر).
String رشته مقدار رشته حفظ می‌شود.
String عدد، بولی به صورت رشته‌ای قالب‌بندی شده است (برای مثال، "42" ، "true" ، "false" ).
String null / undefined null به جاوا null و undefined به رشته‌ی تحت‌اللفظی "undefined" اجبار می‌کند.
String شیء، ArrayBuffer، TypedArray رشته‌ی تحت‌اللفظی "undefined" را اجبار می‌کند.
آرایه اولیه (مانند int[] ، byte[] ، boolean[] ) یا String[] آرایه ( [...] ) به یک آرایه تک بعدی جاوا از نوع عنصر هدف تبدیل می‌شود. آرایه‌های پراکنده، اندیس‌های اختصاص داده نشده را با مقادیر پیش‌فرض ( 0 ، false ، null ) پر می‌کنند.
آرایه اولیه (مانند int[] ، byte[] ) آرایه نوع‌بندی‌شده ( Int8Array ، Uint8Array ، Int32Array ، Float64Array ) عناصر به اجبار در آرایه اولیه جاوا مربوطه قرار می‌گیرند.
آرایه چند بعدی (مانند int[][] ) آرایه تو در تو ( [[...]] ) پشتیبانی نمی‌شود. پارامترهای آرایه چندبعدی مقدار null را ارزیابی می‌کنند.
ArrayBuffer ، DataView ArrayBuffer ، DataView به عنوان آرایه پشتیبانی نمی‌شود. نمونه‌های ArrayBuffer و DataView مقدار null را ارزیابی می‌کنند.
Object یا کلاس سفارشی شیء جاوا اسکریپت ( {...} ) پشتیبانی نمی‌شود. مقادیر دلخواه شیء جاوا اسکریپت در جاوا به null ارزیابی می‌شوند.
Object یا کلاس سفارشی پوشش دهنده شیء جاوا تزریق شده پشتیبانی شده (رفت و برگشت). نمونه جاوای زیرین را به متد جاوا ارسال می‌کند. اگر نوع جاوا با امضای پارامتر مطابقت نداشته باشد، یک استثنای جاوا اسکریپت ایجاد می‌کند.
انواع جعبه‌ای (مانند Integer ، Double ، Boolean ) عدد، بولی پشتیبانی نمی‌شود. انواع اولیه‌ی جعبه‌ای به عنوان اشیاء مات در نظر گرفته می‌شوند و مقدار null را دریافت می‌کنند.
هر نوع اولیه null / undefined مقادیر پیش‌فرض ( 0 ، 0.0 ، \u0000 ، false ) را اجبار می‌کند.
Object ، String ، آرایه null جاوا را به null وادار می‌کند.
نگاشت نوع بازگشتی (جاوا به جاوا اسکریپت)

وقتی یک متد جاوا یا کاتلینِ حاشیه‌نویسی‌شده مقداری را برمی‌گرداند، پل آن را به یک نوع جاوااسکریپت تبدیل می‌کند:

نوع بازگشتی جاوا مقدار جاوا اسکریپت typeof جاوا اسکریپت
boolean true / false "boolean"
byte ، short ، int ، long ، float ، double شماره "number"
char شماره (کد نقطه یونیکد) "number"
String (غیر تهی) مقدار رشته‌ای "string"
String ( null ) undefined "undefined"
void undefined "undefined"
آرایه جاوا (مانند int[] ، String[] ) undefined "undefined" . مقادیر بازگشتی آرایه پشتیبانی نمی‌شوند. متد جاوا اجرا نمی‌شود و undefined بدون ایجاد استثنا بازگردانده می‌شود.
شیء جاوا / نوع سفارشی (غیر تهی) پوشش دهنده شیء "object" . یک پوشش جاوااسکریپت در اطراف نمونه جاوا ایجاد می‌کند. کد جاوااسکریپت می‌تواند هر متد عمومی را روی این شیء که با @JavascriptInterface حاشیه‌نویسی شده است، فراخوانی کند.
شیء جاوا / نوع سفارشی ( null ) null "object"
انواع اولیه جعبه‌ای (مانند Integer ، Double ) پوشش دهنده شیء "object" . به صورت یک پوشش شیء جاوای غیرشفاف بدون هیچ متد قابل دسترسی @JavascriptInterface برگردانده می‌شود و مقدار را در جاوا اسکریپت غیرقابل استفاده می‌کند.

دسترسی‌پذیری متد و عضو

پل جاوا اسکریپت، قوانین سختگیرانه‌ای برای دسترسی اعضا و قابلیت مشاهده اعمال می‌کند تا از اجرای ناخواسته کد جلوگیری کند:

  • فیلدها نمایش داده نمی‌شوند : فیلدهای جاوا (از جمله فیلدهای public و public final ) از طریق جاوا اسکریپت قابل دسترسی نیستند و به صورت undefined ارزیابی می‌شوند.
  • الزام حاشیه‌نویسی : فقط متدهایی که صریحاً با @JavascriptInterface حاشیه‌نویسی شده‌اند، در معرض جاوا اسکریپت قرار می‌گیرند.
  • محدودیت‌های دید : متدها باید public باشند. متدهای private و protected هرگز در معرض جاوااسکریپت قرار نمی‌گیرند، حتی اگر حاشیه‌نویسی @JavascriptInterface را داشته باشند.
  • متدهای استاتیک : متدهای استاتیک که با @JavascriptInterface حاشیه‌نویسی شده‌اند، از جاوا اسکریپت قابل فراخوانی هستند.
  • وراثت و بازنویسی : حاشیه‌نویسی‌های @JavascriptInterface زمانی که یک زیرکلاس یک متد را بازنویسی می‌کند، به ارث برده نمی‌شوند. اگر یک زیرکلاس یک متد حاشیه‌نویسی‌شده از یک ابرکلاس را بازنویسی کند، زیرکلاس باید صریحاً حاشیه‌نویسی @JavascriptInterface را در متد بازنویسی‌شده قرار دهد تا آن را در معرض جاوااسکریپت قرار دهد. متدهای عمومی بازنویسی‌نشده که از یک ابرکلاس به ارث رسیده‌اند، در صورت حاشیه‌نویسی در ابرکلاس، قابل دسترسی باقی می‌مانند.
  • محافظت در برابر بازتاب : متدهای استاندارد بازتاب جاوا (مانند getClass() ) مسدود شده و یک استثنای جاوا اسکریپت ایجاد می‌کنند تا از آسیب‌پذیری‌های اجرای کد از راه دور جلوگیری شود.
  • سربارگذاری متد : متدهای جاوای سربارگذاری‌شده پشتیبانی می‌شوند. پل، فراخوانی‌های متد را فقط بر اساس تعداد آرگومان‌های ارسالی حل می‌کند و انواع آرگومان‌ها را در نظر نمی‌گیرد. فراخوانی یک متد سربارگذاری‌شده با تعداد آرگومان نامعتبر، یک خطای جاوا اسکریپت ایجاد می‌کند. اگر دو سربارگذاری تعداد آرگومان یکسانی داشته باشند، یکی به طور دلخواه انتخاب می‌شود.

خلاصه مکانیسم‌ها

جدول زیر مقایسه‌ای سریع از سه مکانیسم اصلی پیاده‌سازی پل بومی ارائه می‌دهد:

روش addWebMessageListener postWebMessage addJavascriptInterface
پیاده‌سازی ناهمزمان (شنونده روی نخ اصلی) ناهمزمان همزمان
امنیت بالاترین (مبتنی بر لیست مجاز) بالا (آگاه از مبدا) کم (بدون بررسی مبدا)
پیچیدگی متوسط متوسط ساده
جهت دو جهته دو جهته وب به اپلیکیشن
حداقل نسخه وب ویو نسخه ۸۲ (و Jetpack Webkit 1.3.0) نسخه ۴۵ (و Jetpack Webkit 1.1.0) همه نسخه‌ها
توصیه شده بله خیر خیر

انتقال داده‌های بزرگ را مدیریت کنید

هنگام انتقال بارهای داده بزرگ، مانند رشته‌های چند مگابایتی یا فایل‌های باینری، باید حافظه را با دقت مدیریت کنید تا از خطاهای عدم پاسخگویی برنامه (ANR) یا خرابی در دستگاه‌های 32 بیتی جلوگیری شود. این بخش به بررسی تکنیک‌ها و محدودیت‌های مختلف مرتبط با انتقال حجم قابل توجهی از داده‌ها بین برنامه میزبان و محتوای وب می‌پردازد.

انتقال داده‌های دودویی با آرایه‌های بایتی

با کلاس WebMessageCompat ، می‌توانید آرایه‌های byte[] را مستقیماً به جای سریال‌سازی داده‌های دودویی به رشته‌های Base64 ارسال کنید. از آنجایی که Base64 تقریباً 33٪ سربار به اندازه داده‌ها اضافه می‌کند، این روش به طور قابل توجهی از نظر حافظه کارآمدتر و سریع‌تر است.

  • مزیت دودویی : انتقال داده‌های دودویی مانند فایل‌های تصویری یا صوتی بین برنامه بومی و محتوای وب شما.
  • محدودیت : حتی با آرایه‌های بایتی، سیستم داده‌ها را در مرز ارتباط بین فرآیندی (IPC) بین برنامه و فرآیند ایزوله‌ای که WebView برای رندر محتوای وب استفاده می‌کند، کپی می‌کند. این روش هنوز هم برای فایل‌های بسیار بزرگ، حافظه قابل توجهی را مصرف می‌کند.

مثال‌های کد زیر نحوه تنظیم addWebMessageListener در سمت برنامه بومی نشان می‌دهند تا پیام‌های علامت‌گذاری شده با WebMessageCompat.TYPE_ARRAY_BUFFER را دریافت کرده و به صورت اختیاری با بررسی WebViewFeature.MESSAGE_ARRAY_BUFFER با داده‌های دودویی پاسخ دهد.

کاتلین

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
        )
    }
}

جاوا

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
      );
  }
}

کد جاوا اسکریپت زیر پیاده‌سازی 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. برنامه اندروید این درخواست را در WebViewClient .shouldInterceptRequest رهگیری می‌کند.
  3. برنامه داده‌ها را به عنوان یک InputStream برمی‌گرداند.

این امر امکان پخش داده‌ها را به صورت تکه‌ای فراهم می‌کند، به جای اینکه کل بار داده به طور همزمان در حافظه بارگذاری شود.

تابع جاوا اسکریپت زیر، کد سمت کلاینت را برای بارگذاری کارآمد یک فایل باینری بزرگ از برنامه بومی با استفاده از فراخوانی استاندارد fetch() به یک URL سفارشی و placeholder نشان می‌دهد.

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 در کاتلین و جاوا، یک فایل باینری بزرگ را با رهگیری یک URL سفارشی درخواست شده توسط محتوای وب، استریم می‌کند.

کاتلین

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)
    }
}

جاوا

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 ارائه شده در callback پیام است. از استفاده از wildcard کامل ( * ) که با همه originها مطابقت دارد، به عنوان تنها قانون origin خود، مگر در موارد ضروری، خودداری کنید. استفاده از wildcardها برای زیر دامنه‌ها (به عنوان مثال، *.example.com ) برای تطبیق چندین زیر دامنه (به عنوان مثال، foo.example.com ، bar.example.com ) معتبر و ایمن باقی می‌ماند.

    توجه : اگرچه قوانین مبدا در برابر وب‌سایت‌های شخص ثالث مخرب و iframe های پنهان محافظت می‌کنند، اما نمی‌توانند در برابر آسیب‌پذیری‌های اسکریپت‌نویسی بین سایتی (XSS) در دامنه مورد اعتماد شما محافظت کنند. به عنوان مثال، اگر صفحه وب شما محتوای تولید شده توسط کاربر را نمایش می‌دهد و در برابر XSS ذخیره شده آسیب‌پذیر است، یک مهاجم می‌تواند اسکریپتی را اجرا کند که به عنوان مبدا مورد اعتماد شما عمل می‌کند. قبل از اجرای عملیات حساس پلتفرم بومی، اعتبارسنجی را برای بارهای داده پیام در نظر بگیرید.

  • به حداقل رساندن مساحت سطح : فقط روش‌ها یا داده‌های خاصی را که صفحه وب نیاز دارد، نمایش دهید.

  • بررسی ویژگی‌ها در زمان اجرا : APIهای پل اخیر، از جمله addWebMessageListener ، بخشی از کتابخانه Jetpack Webkit هستند. بنابراین، همیشه قبل از فراخوانی آنها، با استفاده از WebViewFeature.isFeatureSupported() پشتیبانی آنها را بررسی کنید.