UIWebview、WKWebView中JS交互方法总结

本文主要总结一下JS与原生交互的几种方式,其中包括UIWebview与WKWebView这两个iOS端加载H5的控件。这类文章在网上其实也有很多,但是都只是介绍 iOS 这边怎么处理的。这样在和 H5 联调时产生问题的时候就比较浪费时间,所以我这边把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写的太乱就不贴链接了,有需要可以找我要。

下面是我对 WebViewJavascriptBridge 源码的学习,有兴趣的同学可以看看哦。
WebViewJavascriptBridge 源码学习--了解其实现原理

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 216,193评论 6 498
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 92,306评论 3 392
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 162,130评论 0 353
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 58,110评论 1 292
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 67,118评论 6 388
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 51,085评论 1 295
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,007评论 3 417
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 38,844评论 0 273
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 45,283评论 1 310
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 37,508评论 2 332
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 39,667评论 1 348
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,395评论 5 343
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,985评论 3 325
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 31,630评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,797评论 1 268
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 47,653评论 2 368
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 44,553评论 2 352

推荐阅读更多精彩内容