این صفحه به بررسی روشها و بهترین شیوههای مختلف برای ایجاد یک پل بومی، که با نام پل جاوا اسکریپت نیز شناخته میشود، میپردازد تا ارتباط بین محتوای وب در یک WebView و یک برنامه اندروید میزبان را تسهیل کند.
این امر توسعهدهندگان وب را قادر میسازد تا از جاوا اسکریپت برای دسترسی به ویژگیهای بومی پلتفرم - مانند دوربین، سیستم فایل یا حسگرهای سختافزاری پیشرفته - که APIهای استاندارد وب معمولاً ارائه نمیدهند، استفاده کنند.
موارد استفاده
پیادهسازی پل جاوااسکریپت، سناریوهای مختلف ادغام را که در آنها محتوای وب نیاز به دسترسی عمیقتر به سیستم عامل اندروید دارد، امکانپذیر میکند. در زیر چند نمونه آورده شده است:
- یکپارچهسازی پلتفرم : فعالسازی اجزای رابط کاربری بومی اندروید (برای مثال، Biometric prompts،
BottomSheetDialog) از یک صفحه وب. - عملکرد : واگذاری وظایف محاسباتی سنگین به کد بومی جاوا یا کاتلین.
- ماندگاری دادهها : دسترسی به پایگاههای داده رمزگذاری شده محلی یا تنظیمات برگزیده مشترک.
- انتقال دادههای حجیم : انتقال فایلهای رسانهای یا ساختارهای داده پیچیده بین برنامه و رندرکننده وب.
مکانیسمهای ارتباطی
اندروید سه نسل اصلی از APIها را برای ایجاد یک پل بومی ارائه میدهد. اگرچه همه آنها هنوز در دسترس هستند، اما از نظر امنیت، قابلیت استفاده و عملکرد تفاوتهای چشمگیری دارند.
استفاده از addWebMessageListener (توصیه میشود)
addWebMessageListener مدرنترین و توصیهشدهترین رویکرد برای ارتباط بین محتوای وب و کد برنامه بومی است. این رویکرد، سهولت استفاده از رابط جاوا اسکریپت را با امنیت سیستم پیامرسانی ترکیب میکند.
نحوه کار : برنامه یک شنونده با نام خاص و مجموعهای از قوانین مبدا مجاز اضافه میکند. سپس WebView از لحظه شروع بارگذاری صفحه، اطمینان حاصل میکند که شیء جاوا اسکریپت در محدوده سراسری ( window.objectName ) وجود دارد.
مقداردهی اولیه : برای اطمینان از اینکه WebView قبل از اجرای هر اسکریپتی، شیء جاوا اسکریپت را تزریق میکند، باید قبل از فراخوانی loadUrl() تابع addWebMessageListener فراخوانی کنید.
ویژگیهای کلیدی :
امنیت و اعتماد : برخلاف APIهای قدیمی، این روش در طول مقداردهی اولیه به
Set<String>ازallowedOriginRulesنیاز دارد. این مکانیسم اصلی برای ایجاد اعتماد است.وقتی یک منبع معتبر مانند
https://example.comرا مشخص میکنید، WebView تضمین میکند که اشیاء جاوا اسکریپت تزریق شده را فقط در صفحات وب بارگذاری شده از همان منبع دقیق نمایش دهد.تابع فراخوانی شنونده نیتیو (native listener) با هر پیام، یک پارامتر
sourceOriginدریافت میکند. اگر بریج شما از چندین مبدا مجاز پشتیبانی میکند، میتوانید از این برای تأیید مبدا دقیق فرستنده استفاده کنید.از آنجا که وبویو این بررسیهای مبدا را در سطح پلتفرم به شدت اجرا میکند، برنامه شما میتواند به طور کلی به پیامهای دریافتی از یک
sourceOriginمعتبر به عنوان پیامهای معتبر تکیه کند و نیاز به اعتبارسنجی دقیق بار داده را در اکثر پیادهسازیهای استاندارد از بین ببرد.- وب ویو قوانین را با طرح (HTTP/HTTPS)، میزبان و پورت مطابقت میدهد.
- وب ویو مسیرها را نادیده میگیرد. برای مثال،
https://example.comhttps://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 کنید تا ساختار داده بازسازی شود.
مثال کاربرد :
برای درک کامل توالی تبادل پیام دو طرفه، رویدادها به این ترتیب ادامه مییابند:
- شروع (برنامه) : برنامه بومی شنونده را با
addWebMessageListenerثبت میکند و صفحه وب را باloadUrl()بارگذاری میکند. - ارسال پیام (وب) : جاوا اسکریپت صفحه وب،
myObject.postMessage(message)را برای شروع ارتباط فراخوانی میکند. - دریافت و پاسخ پیام (برنامه) : برنامه پیام را در callback شنونده دریافت میکند و با استفاده از
replyProxy.postMessage()ارائه شده پاسخ میدهد. - دریافت پاسخ (وب) : صفحه وب پاسخ ناهمزمان را در تابع فراخوانی
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()تکیه کنید، زیرا تضمینی برای دقت آنها وجود ندارد و مشخص نمیکنند که کدام فریم خاص درخواست را انجام داده است.
خلاصه مکانیسمها
جدول زیر مقایسهای سریع از سه مکانیسم اصلی پیادهسازی پل بومی ارائه میدهد:
| روش | 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 برای استریم دادهها استفاده کنید:
- صفحه وب، فراخوانی تابع
fetch()را به یک آدرس اینترنتی (URL) سفارشی و جاگذاریشده آغاز میکند. برای مثال،https://app.local/large-file. - برنامه اندروید این درخواست را در
WebViewClient.shouldInterceptRequest رهگیری میکند. - برنامه دادهها را به عنوان یک
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()پشتیبانی آنها را بررسی کنید.