切换语言为:繁体

原生APP WebView 与 h5 通讯设计

  • 爱糖宝
  • 2024-09-08
  • 2056
  • 0
  • 0

一、背景

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

0条评论

您的电子邮件等信息不会被公开,以下所有项均必填

OK! You can skip this field.