美文网首页
iOS WKWebView 与 JS 交互

iOS WKWebView 与 JS 交互

作者: ZT_Story | 来源:发表于2020-01-03 17:04 被阅读0次

    应对苹果公司的号召,2020年还是要把之前老项目的UIWebView都替换成WKWebView。
    单纯换View倒也不难,除了代理方法有点区别之外,加载网页的使用方式都是类似的。
    但现在越来越多应用都采用混合开发的模式,所以就需要Native与JS之间进行良好的通信。
    之前UIWebView与JS通信是采用JavaScriptCore来实现的
    通过给JSContext动态注入OC对象来实现JS调用Native
    由于以后苹果不允许使用UIWebView我这里也就不多讲述其实现了
    主要还是说一说WKWebView是如何与JS之间交互的!

    开讲之前

    今天我们从两个方面来讲述OC与JS交互:
    1、通过原生的WKWebViewConfiguration
    2、通过使用WebViewJavascriptBridge

    通过Native调用JS的方式,没有任何争议,几乎都是用WebView提供的

    [webView evaluateJavaScript:@"" completionHandler:^(id _Nullable result, NSError * _Nullable error) {
            
    }];
    

    这里我们重点讨论JS如何调用Native

    WKWebViewConfiguration、WKUserContentController

    image.png

    WKWebViewConfiguration对象有一个很关键的属性参数userContentController
    我们可以把它当做内容交互控制器,可以自己注入JS代码及JS调用原生方法注册

    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    

    addScript需要与remove成对出现,在dealloc时需要注意调用

    - (void)removeScriptMessageHandlerForName:(NSString *)name;
    

    在JS里我们可以通过

    window.webkit.messageHandlers.<注册的方法名>.postMessage(<需要传递的参数>);
    

    的方式来调用我们在Native端注册的方法
    同时Native会通过WKScriptMessageHandler代理方法

    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
    

    接收JS传递过来的参数,参数内容都封装在WKScriptMessage对象中
    其中,name为注册的方法名,body为传递的参数对象
    由于只能接收一个参数,所以JS通常采取Object转JSON字符串的形式传递多参数,Native这边用NSDictionary接收

    WebViewJavascriptBridge

    这是github上非常火的一个桥接库https://github.com/marcuswestin/WebViewJavascriptBridge有12.9k的Star
    看了看他的源码,整个处理的非常优雅
    主要是利用iframe设置src的方式通知Native完成通信过程
    所以从UIWebView到WKWebView的改动几乎很小,并且还支持相互的通信之间的回调,可以说是非常全面的Bridge了
    我们在这里简单的分析一下这个库是如何实现通信的

    简易流程图
    开始之前我们先看一下这个库的文件结构
    image.png
    整个库总共也就八个文件,算是十分精简也是十分清晰了
    核心代码在WebViewJavascriptBridge_JSWebViewJavascriptBridgeBase
    前者负责实现注入JS的通信解析对象
    后者负责实现Native解析JS信息的对象
    其余两个类文件是用来扩展到UIWebView和WKWebView的,主要是实现对应的协议方法,用作拦截URL
    JS调用Native

    打开WebView页面时,默认通过iframe设置

    WVJBIframe.src = 'https://__bridge_loaded__';
    

    客户端在拦截到初始化的指令时会通过这两步进行预设好的JS注入

    if ([_base isBridgeLoadedURL:url]) {
        [_base injectJavascriptFile];
    }
    ......
    - (void)injectJavascriptFile {
        NSString *js = WebViewJavascriptBridge_js();
        [self _evaluateJavascript:js];
    }
    

    初始化工作做好之后,按照流程就是Native端的注册方法

    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
         NSLog(@"testObjcCallback called: %@", data);
         responseCallback(@"Response from testObjcCallback");
    }];
    

    等待JS端的调用
    JS通过bridge对象的callHandler方法

    var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
         callbackButton.innerHTML = 'Fire testObjcCallback'
         callbackButton.onclick = function(e) {
         bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
              log('JS got response', response)
         })
    }
    ......
    
    function callHandler(handlerName, data, responseCallback) {
         if (arguments.length == 2 && typeof data == 'function') {
                responseCallback = data;
                data = null;
         }
         _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }  
    function _doSend(message, responseCallback) {
         if (responseCallback) {
                var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
                responseCallbacks[callbackId] = responseCallback;
                message['callbackId'] = callbackId;
         }
         sendMessageQueue.push(message);
         messagingIframe.src = 'https://__wvjb_queue_message__/';
    }
    

    拦截URL变化主要是通过WebView的delegate获取

    /// UIWebView 通过
    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
    /// WKWebView 通过
    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
    

    设置了src为https://__wvjb_queue_message__/,Native拦截之后通过获取sendMessageQueue中的message完成方法名的提取和参数的提取

    [_webView evaluateJavaScript:[_base webViewJavascriptFetchQueyCommand] completionHandler:^(NSString* result, NSError* error) {
            if (error != nil) {
                NSLog(@"WebViewJavascriptBridge: WARNING: Error when trying to fetch data from WKWebView: %@", error);
            }
            [_base flushMessageQueue:result];
    }];
    

    通过messageJSONString中的信息匹配到之前注册的方法名和block,调用执行。
    如果Native注册的方法中有返回responseCallBack信息,则将返回的信息参数与JS提供的callBackId组成新的对象,通知JS解析,JS通过callBackId找到对应方法并传递参数执行

    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
    [self _queueMessage:msg];
    

    以上就是完整的JS调用Native流程

    Native调用JS

    反过来,Native调用JS,其实双方注册方法和调用方法都是类似的,整个流程反一下就可以了,具体我这里就不细说了,只要前面的理解了,后面就很容易想明白了,可以参考源码Demo。

    总结一下

    1、JS通知Native信息变化通过

    iframe.src='https://__bridge_loaded__/'                  // 初始化
    iframe.src='https://__wvjb_queue_message__/'            // 发送消息
    

    2、Native通知JS传递参数通过

    [webView evaluateJavaScript:@"" completionHandler:^(id _Nullable result, NSError * _Nullable error) {}];
    

    整个交互过程依赖双方存储注册方法信息,并通过方法名和参数来完成跨界调用

    这个第三方库考验开发者要有一些基础的前端开发经验,否则对于里面近半数的JS源码可能不太理解,重点是设置iframe.src

    以上就是本次我想分享的关于WKWebView与JS交互的内容
    ps:内容建议结合源码Demo一起食用,更容易理清思路
    希望对你有帮助

    相关文章

      网友评论

          本文标题:iOS WKWebView 与 JS 交互

          本文链接:https://www.haomeiwen.com/subject/lwfioctx.html