一、背景
APP 套殼 webview h5, 兩者的通訊實現不難,如何設計的簡便,易於擴充套件方便的和 h5 的 開發框架結合相對比較重要。
二、IOS的實現
(一)、端程式碼實現
1、端實現配置 webview , 注入 js 指令碼
// 建立 WKWebViewConfiguration 物件,這是 WKWebView 的配置類,用於設定各種引數 WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]; // 建立 WKUserContentController 物件,這個控制器用於管理使用者指令碼和訊息處理 config.userContentController = [[WKUserContentController alloc] init]; // 註冊一個指令碼訊息處理器,這裏註冊了一個名為 AppModel 的處理器,用於處理 JavaScript 向原生髮送的訊息 // 當JS透過AppModel來呼叫時,我們可以在WKScriptMessageHandler代理中接收到 [config.userContentController addScriptMessageHandler:self name:@"AppModel"]; // 獲取應用的版本號,從應用的 Info.plist 檔案中讀取 CFBundleVersion 鍵的值 NSString *appBuild = [[NSBundle mainBundle] objectForInfoDictionaryKey:@"CFBundleVersion"]; // 定義要注入到 window 物件的 JavaScript 程式碼,這裏注入了一些全域性變數 NSString *jsString = [NSString stringWithFormat:@"window.__iosApp__ = true; window.iosBuildCode = %@",appBuild]; // 建立一個 WKUserScript 物件,用於將定義的 JavaScript 程式碼注入到網頁中 WKUserScript *noneSelectScript = [[WKUserScript alloc] initWithSource:jsString injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO]; // 將自定義的 JavaScript 指令碼新增到 WKUserContentController 中 [config.userContentController addUserScript:noneSelectScript];
2、實現 native 呼叫 h5 的方法
native 呼叫 h5 的實現思路是 透過呼叫一個方法,傳入事件和引數,在h5內在分發處理不同的事件,達到簡化程式碼的目的
- (void)channelMessage:(NSString *)event withData:(NSString *)data { NSLog(@"data: %@",data); NSString *jsCode = [NSString stringWithFormat:@"channelMessage('%@', '%@')", event, data]; // 呼叫執行js方法 [self.webView evaluateJavaScript: jsCode completionHandler:^(id _Nullable response, NSError * _Nullable error) { if (error) { NSLog(@"Error calling JavaScript: %@", error.localizedDescription); } else { if ([response isEqualToString:@"sucess"]) { NSLog(@"方法執行成功"); } } }]; };
3、實現處理 h5 發來的事件請求
端上實現 h5的請求是透過 WKScriptMessageHandler協議方法實現的
@interface ViewController ()<WKScriptMessageHandler>
實現WKScriptMessageHandler協議方法 實現 h5 傳送訊息呼叫 native 的方法, 透過接收物件,內部包含 message 和 其他需要的引數物件, 分發事件, 如下的呼叫購買、呼叫下載、 開啟聯絡人app
// 3、實現WKScriptMessageHandler協議方法 實現 h5 傳送訊息呼叫 native 的方法 #pragma mark - WKScriptMessageHandler - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"AppModel"]) { // 假設message.body是一個字串,首先將其轉換為NSDictionary NSDictionary *messageData = [NSJSONSerialization JSONObjectWithData:[((NSString *)message.body) dataUsingEncoding:NSUTF8StringEncoding] options:0 error:nil]; // 現在你可以訪問param1和param2了 NSString *message = messageData[@"message"]; // h5 呼叫native 購買的方法 if ([message isKindOfClass:[NSString class]] && [message isEqualToString:@"handleBuy"]) { NSString *productId = messageData[@"productId"]; [self buyClickWithProductID:productId]; } // h5 呼叫native 下載的方法 if ([message isKindOfClass:[NSString class]] && [message isEqualToString:@"downLoadVideo"]) { NSString *videoUrl = messageData[@"videoUrl"]; [self downLoadVideo:videoUrl]; } // h5 呼叫 native 開啟聯絡人app if([message isKindOfClass:[NSString class]] && [message isEqualToString:@"sharedApplication"]) { NSURL *url = [NSURL URLWithString:messageData[@"url"]]; NSURL *appStoreUrl = [NSURL URLWithString:messageData[@"appStoreUrl"]]; // 檢查應用是否可開啟 if ([[UIApplication sharedApplication] canOpenURL:url]) { // 應用已安裝,開啟應用 [[UIApplication sharedApplication] openURL:url options:@{} completionHandler:nil]; } else { [[UIApplication sharedApplication] openURL:appStoreUrl options:@{} completionHandler:nil]; } } NSLog(@"message: %@",message); } }
(二)、H5 實現通訊
1、h5 實現主要是通 channelMessage 接收 端發過來的訊息 ( 端上設定呼叫這個函式 )
window.channelMessage = function channelMessage(event: ListenerNativeMessageKey, message?: string) { // 接收處理訊息 return "success"; };
2、透過 window.webkit.messageHandlers.AppModel.postMessage
向端傳送訊息
if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.AppModel) { window.webkit.messageHandlers.AppModel.postMessage(messageDataStr); } else { console.log("The native context does not exist."); }
3、透過釋出訂閱模式整合到專案中
爲了更好的後續開發,透過釋出訂閱模式,管理事件的傳送與接收,透過ts 定義好不同的型別,方便檢視和使用時的提示等
// 匯入釋出訂閱模式 import iosPubSub from "./iosPubSub"; //... // 初始化呼叫下面程式碼 function sendMessageToNative(message, params = {}) { const messageData = { ...params, message, }; const messageDataStr = JSON.stringify(messageData); if (window.webkit && window.webkit.messageHandlers && window.webkit.messageHandlers.AppModel) { window.webkit.messageHandlers.AppModel.postMessage(messageDataStr); } else { console.log("The native context does not exist."); } } // native 傳送 h5 訊息 iosPubSub.subscribe(messageKey.sendToNative, sendMessageToNative); // 監聽 native 傳送來的訊息 event 為訊息型別 window.channelMessage = function channelMessage(event: ListenerNativeMessageKey, message?: string) { iosPubSub.publish(event, message); return "success"; }; //...
解釋: 在初始化時,訂閱 messageKey.sendToNative 這個事件,繫結一個函式,傳入要傳送的事件名和引數物件。 在使用的時候如下
// 呼叫 native 的購買, 傳遞事件,和引數 iosBridge.pubSub.publish(messageKey.sendToNative, messageKey.handleBuy, { productId: productId });
透過 window.channelMessage 接收端傳送的訊息, 在需要用到的地方 訂閱, 比如訂閱 messageKey.messageNotification 接收端傳送的訊息, 在銷燬時取消此事件的訂閱
useEffect(() => { iosBridge.pubSub.subscribe(messageKey.messageNotification, messageNotification); return () => { iosBridge.pubSub.unsubscribe(messageKey.messageNotification, messageNotification); }; }, []);
透過 messageKey 的ts 物件定義內部的事件及型別傳參,方便管理不同的事件
三、Android 的實現
(一)、端程式碼實現
1、實現webview的配置
透過 WebAppInterface(this),實現 JavaScript 可以呼叫的方法,在 JavaScript 透過"Android"
中用於訪問這個物件的介面名稱
webView.addJavascriptInterface(WebAppInterface(this), "Android")
2、實現 native 呼叫 h5 方法
fun callJsFromAndroid(funName: String, eventType: String, message:String?="") { // 確保在主執行緒中呼叫evaluateJavascript if (Looper.myLooper() == Looper.getMainLooper()) { handleEvaluateJs(funName, eventType, message) } else { // 如果不在主執行緒,使用Handler切換到主執行緒 Handler(Looper.getMainLooper()).post { handleEvaluateJs(funName, eventType, message) } } } fun handleEvaluateJs (funName: String, eventType: String, message:String?){ webView.evaluateJavascript("javascript:$funName('$eventType', '$message')") { value -> // 處理 JavaScript 函式返回的結果 if (value != null) { // 如果返回值不為空,執行一些操作 println("JavaScript 返回值: $value") } } }
⚠️ 在 webview 的通訊中, 要確保呼叫js 是在主執行緒進行,仍然是在呼叫 js 時將事件型別和引數進行傳遞
3、實現接收 h5 發來的事件
inner class WebAppInterface(private val activity:ComponentActivity) { /** * js 呼叫native的購買 傳入對是物件字串 透過import org.json.JSONObject解構物件 */ @JavascriptInterface fun handleBuy(productAndUser: String) { // 將字串轉換為 JSONObject val jsonObj = JSONObject(productAndUser) val productId = jsonObj.getString("productId") val mainSecId = jsonObj.getString("mainSecId") toGooglePay(productId, mainSecId) } }
安卓端接收事件 是透過 WebAppInterface 類實現的 安全考慮配置 @JavascriptInterface 註解
(二)、H5 實現通訊
1、h5 依然是透過 window物件上掛在的 channelMessage 接收 native 傳送來的訊息, 透過publish 觸發事件
window.channelMessage = function channelMessage(event: ListenerNativeMessageKey, message?: string) { androidPubSub.publish(event, message); return "success"; };
在用到的頁面訂閱接收
function showToast(message: string) { Taro.showToast({ title: t(message), }); } //... useEffect(() => { androidBridge.pubSub.subscribe(androidBridgeMessage.showToast, showToast); return () => { androidBridge.pubSub.unsubscribe(androidBridgeMessage.showToast, showToast); }; }, []);
2、透過 window.Android[messageType]?.(paramsStr) 向 native 傳送事件
function sendMessageToNative(messageType, params = {}) { const paramsStr = JSON.stringify(params); if (window.Android) { try { window.Android[messageType]?.(paramsStr); } catch (error) {} } else { console.log("The native context does not exist."); } } // native 傳送 h5 訊息 androidPubSub.subscribe(messageKey.sendToNative, sendMessageToNative);
依然是透過釋出訂閱模式與專案進行整合, 在使用的時候 透過 呼叫 messageKey.sendToNative 向native 傳送事件和引數
三、總結
APP 的端內webview與 內嵌的 H5 方案的通訊除了各自透過 介面實現外, 還需要我們有一套更好的方式管理兩端的通訊, 尤其注意webview的通訊要在主執行緒,否則會遇到傳送給h5可以收到,h5 再返回無法收到訊息的情況。 另外就是怎麼能簡化方便的管理事件方法, 與移動端框架更好的結合,也就是訊息驅動檢視更新。