H5与原生交互总结
本文主要总结一下JS与原生交互的几种方式,其中包括UIWebview与WKWebView这两个iOS端加载H5的控件。
UIWebview
H5调用原生方法
1、通过拦截加载的URL。
相信iOS的小伙伴应该都知道Delegate这个玩意儿,在UIWebview中就存在UIWebViewDelegate这个玩意儿,拦截URL就是通过代理中的一个方法实现的。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
就是上面这个玩意儿。这个方法就是当WebView在加载url之前会执行。这时候我们就可以在加载他之前搞事情啦,具体的怎么玩儿,看代码。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
//获取url里的相关参数(就是把URL里?后面的玩意儿取出来)
//如下:
//http://127.0.0.1:8080/index.html?message=ocuiwebviewurl拦截&flag=yjkwap://wjj.com&action=helloOC'
//这个玩意儿获取到的 dict = {@"message":@"ocuiwebviewurl拦截", @"flag":@"yjkwap://wjj.com", @"action":@"helloOC"}
NSDictionary *dict = [YJKTool urlPramaDictionaryWithUrlString:request.URL.absoluteString];
//存在约定的规则就进行拦截(这里约定的规则就是flag=yjkwap://wjj.com)
if ([[dict valueForKey:@"flag"] isEqualToString:@"yjkwap://wjj.com"]) {
//执行相关的动作(action代表要执行的动作,就是要调用原生的啥方法)
if ([[dict valueForKey:@"action"] isEqualToString:@"helloOC"]) {
// message 就是H5给我们传递过来的参数
//在这里搞事情
// ... 此处省略十来行代码 ...
// 最后要阻止页面的加载
return NO;
}
}
return YES;
}
就是这么简单,注释写的已经很清楚了,应该是都可以看懂的哦。
既然原生这边搞定了,还有一个问题就是H5那边怎么写呢? 就下面这句代码,扔给你们H5。
location.href = 'http://127.0.0.1:8080/deeplink.html?message=ocuiwebviewurl拦截&flag=yjkwap://wjj.com&action=helloOC';
2、系统自带JavaScriptCore库里的,JSContext类。
这个是系统类库里的玩意儿,用起来也是比较简单方便的,这个和上面方法的处理时机不太一样,是在另一个代理方法里面实现的。
- (void)webViewDidFinishLoad:(UIWebView *)webView
这个方法就是当WebView在加载完成之后执行的。
还有一点就是通过 JSContext 可以有两种方式去实现交互的功能。
I、通过block回调
没啥好说的,直接上代码吧。
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//获取 JSContext 对象
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//执行约定的方法 通过block回调
__weak JSContext *weakContext = context;
__weak YJKWebViewVC *weakSelf = self;
// helloOC 就是提供给H5调用的方法。
context[@"helloOC"] = ^(){
//注意:这里面是子线程不能做刷新UI
// 获取H5所传递的参数
NSArray *args = [JSContext currentArguments];
//这里是取出参数里面的地一项
JSValue *value = [args firstObject];
//这里是将 JSValue 对象转化为字典,JSValue 的对象还可以转化成NSString、NSNumber等,具体转化为什么类型要看H5那边所传递的参数类型是什么玩意儿
[value toDictionary]
// 搞事情
// ... 此处省略十来行代码 ...
};
}
照例奉上H5代码:
//单一参数,参数为字典
window.helloOC({"message":"OC UIWebview JSContext"})
//多参数,使用逗号分割就好。参数分别为 NSString、Int
window.helloOC('123',123)
注意:这里的 block 是在子线程回调的,不能进行UI操作。
II、挂载在其他对象下
这个和上面实质是一样的,这样做就是为了将交互方法统一管理,使代码更加清晰明了。
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//SwiftJavaScriptModel 就是我新弄的一个类
//挂载对象
[[SwiftJavaScriptModel alloc] initWithJsContext:context];
}
import UIKit
// 定义协议SwiftJavaScriptDelegate 该协议必须遵守JSExport协议
@objc protocol SwiftJavaScriptDelegate: JSExport {
//给H5调用的方法
func helloOC() -> Void
}
@objc class SwiftJavaScriptModel: NSObject, SwiftJavaScriptDelegate {
@discardableResult @objc convenience init(jsContext: JSContext) {
self.init()
//这一步是将SwiftJavaScriptModel模型注入到JS中,在JS就可以通过YJKJSContextObj调用我们暴露的方法了。
jsContext.setObject(self, forKeyedSubscript: "YJKJSContextObj" as NSCopying & NSObjectProtocol)
}
//这个方法的实现逻辑和通过 Block 回调的方式是一样的,就没多些注释哦。
func helloOC() -> Void {
let paramArray = JSContext.currentArguments()
let value:JSValue? = paramArray?.first as? JSValue
if value != nil {
let dict = value!.toDictionary()
print(dict!)
}
}
}
万众期待的H5代码:
//是不是和上面的很像额,他就是在 window 下弄啦个 YJKJSContextObj 去处理和原生的交互。
window.YJKJSContextObj.helloOC({"message":"OC UIWebview JSContext"})
3、iOS同行小伙伴封装的WebViewJavascriptBridge库。
这个库就很优秀了,有时间的同学可以去看看源码,它的实现原理就是通过拦截URL,真的要膜拜大神,脑子真好使,封装出来这么一个小可爱。
到底怎么使用呢?废话不多说,直接上代码。
__weak YJKWebViewVC *weakSelf = self;
//弄一个 WebViewJavascriptBridge 的对象
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.uiWebView];
//处理 WebView 回调用的
[self.bridge setWebViewDelegate:self];
//注册方法给H5用 方法名就是helloOC
[self.bridge registerHandler:@"helloOC" handler:^(id data, WVJBResponseCallback responseCallback) {
//data 是H5传过来的参数
//搞事情
//搞完事情,通过回调返回相应的参数给到H5,比如它调用你的 1+1 的方法,你计算完之后要把 2 给到H5。
responseCallback(@"1111")
}];
注意 1:WebViewJavascriptBridge 的对象不能弄成局部变量,不然他会释放掉,导致 webview 的无法执行。
注意 2:当H5页面刚打开时就要调用原生方法时(比如H5调用原生的数据请求),这时候需要做个延迟处理。(我这边分析的原因可能是在H5调用原生方法时,原生这边的方法还没注册上,因此可能是H5调用方法的时机不太对,有哪位小伙伴知道怎么处理希望可以告诉我哦)
到这里我们这边的事情已经搞定啦,又是无奈的H5教学时间。
首先要在 .JS 文件中添加一下方法,并且调用一下,这里是准备工作。
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge((bridge) => {});
然后就只需要在调用原生方法的时候写出一下代码就好啦
//调用原生注册的 helloOC 方法,参数为 {"message":"11111"}
window.WebViewJavascriptBridge.callHandler('helloOC',{"message":"11111"}, function responseCallback(responseData) {
//这边原生回调信息(responseData),处理自己的逻辑
})
这边的具体原理就不说啦,有时间会写另外一篇文章来详细说明的。
到这里,在 UIWebview 中 H5 调用原生方法的处理方式就总结完成啦,有啥不明白的可以评论或私信哦。
原生调用H5方法
1、UIWebView 执行 JavaScriptString。
原生调用H5就更简单啦,一句话搞定
// webView对象在合适的时机,调用这个方法就行啦。
//入参就是一个JavaScriptString。
//changeString('haahhahah') 他的意思就是调用H5的 changeString 方法,传入参数'haahhahah'。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
H5要为我们做的事情
//我这边使用的是Vue,其他框架大致相同。
mounted:function(){
// 将changeString方法绑定到window下面,提供给外部调用
window['changeString'] = (result) => {
this.aaaa = result
}
}
2、JSContext 执行 JavaScriptString。
// context 这个玩意儿还记得么?就是在页面加载完成时,通过
//JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//代码获取到的
[self.context evaluateScript:@"changeString('haahhahah')"];
H5那边要做的事情和方法一样的哦。
3、iOS同行小伙伴封装的WebViewJavascriptBridge库。
别人的库总是那么好使。
//调用H5 helloJS 方法,参数为nil
[self.bridge callHandler:@"helloJS" data:nil responseCallback:^(id responseData) {
//等待H5的回调然后做事情
NSLog(@"%@",responseData);
}];
H5的小哥哥要为我们做的事情
mounted:function(){
//注册方法给原生调用哦(写在这我也不知道好使不... 自己的demo没验证这个)
window.WebViewJavascriptBridge.registerHandler('helloJS', function(data, responseCallback) {
responseCallback("123")
})
},
关于 UIWebview 的总结就是这些咯,下面要说大家期待的 WKWebView。
WKWebView
从 iOS 8 开始呢,就可以用 WKWebView 了,好处有很多,坑呢也不少。这些网上文章也多,这里就不介绍这些东西了。接下来进入我们这个文章的主题
H5调用原生方法
1、通过拦截加载的URL。
这个和UIWebview那边区别不大,就是拦截URL的代理变掉了。通过下面代码就可以发现喽,代理方法里面执行的逻辑和UIWebview一眼的额。
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
//获取url里的相关参数(就是把URL里?后面的玩意儿取出来)
//如下:
//http://127.0.0.1:8080/index.html?message=ocuiwebviewurl拦截&flag=yjkwap://wjj.com&action=helloOC'
//这个玩意儿获取到的 dict = {@"message":@"ocuiwebviewurl拦截", @"flag":@"yjkwap://wjj.com", @"action":@"helloOC"}
NSDictionary *dict = [YJKTool urlPramaDictionaryWithUrlString:request.URL.absoluteString];
//存在约定的规则就进行拦截(这里约定的规则就是flag=yjkwap://wjj.com)
if ([[dict valueForKey:@"flag"] isEqualToString:@"yjkwap://wjj.com"]) {
//执行相关的动作(action代表要执行的动作,就是要调用原生的啥方法)
if ([[dict valueForKey:@"action"] isEqualToString:@"helloOC"]) {
// message 就是H5给我们传递过来的参数
//在这里搞事情
// ... 此处省略十来行代码 ...
// 最后要阻止页面的加载
decisionHandler(WKNavigationActionPolicyCancel);
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
H5小伙伴写的东西有完全没变化,参照 UIWebview 那边的代码好啦。
2、WKScriptMessageHandler。
先初始化 WKWebView 对象
//弄一个 WKWebViewConfiguration 对象
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//然后 configuration.userContentController 对象初始化一下哦
configuration.userContentController = [[WKUserContentController alloc] init];
//添加 helloOC 方法给H5小伙伴调用哦
[configuration.userContentController addScriptMessageHandler:self name:@"helloOC"];
//下面就是创建 WKWebView 对象的相关代码,没啥好看的哦
self.wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.wkWebView.navigationDelegate = self;
[self.wkWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://192.168.2.50:8080/index.html"]]];
[self.view addSubview:self.wkWebView];
helloOC 方法的具体实现使用下面的代理就好啦
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// message.name 获取到对应的方法名
if ([message.name isEqualToString:@"helloOC"]) {
// message.body H5传过来的参数,具体啥类型要看H5所传参数是啥类型哦
NSDictionary *dict = message.body;
}
}
请教H5的伙伴
//很简单的代码哦
// window.webkit.messageHandlers 这个对象,调用 helloOC 方法,传递相关参数({"message":"OC WKWebview WKScriptMessageHandler"})
window.webkit.messageHandlers.helloOC.postMessage({"message":"OC WKWebview WKScriptMessageHandler"})
注意:这里方法会导致页面的循环引用。解决方法有以下两个哦,喜欢那个就用那个喽
iOS WKWebView导致ViewController不调用dealloc方法
使用WKWebView时,ViewController不走dealloc方法的问题解决方法
3、iOS同行小伙伴封装的WebViewJavascriptBridge库。
这里没有任何变化,大神已做兼容,参照 UIWebview 就好。
原生调用H5方法
1、WKWebView 执行 JavaScriptString。
原生要做的就是下面这句话,H5的做法和 UIWebview 那边一样的。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
2、iOS同行小伙伴封装的WebViewJavascriptBridge库。
这里没有任何变化,大神已做兼容,参照 UIWebview 就好。
到这里就全部总结完成啦,刚开始和H5做交互的时候不是H5调不到原生,就是原生调不到H5,当时大家都是蒙蔽的,浪费很多时间去排查问题。经过这一总结,以后出问题就可以快速定位啦。
希望可以给阅读者提供帮助,有什么问题可以评论或私信,demo写的太乱就不贴链接了,有需要可以找我要。