一、背景
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 再返回无法收到消息的情况。 另外就是怎么能简化方便的管理事件方法, 与移动端框架更好的结合,也就是消息驱动视图更新。