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