美文网首页iOS 常见问题iOS基础iOS 开发
iOS OC与JS交互(四)-- WebViewJavascri

iOS OC与JS交互(四)-- WebViewJavascri

作者: 淡定的笨鸟 | 来源:发表于2019-05-13 23:31 被阅读0次

    本篇笔记记录WebViewJavascriptBridge在WKWebView中的使用和原理分析

    WebViewJavascriptBridge的方法介绍
    //为WKWebView添加与JS沟通的bridge桥梁
    + (instancetype)bridgeForWebView:(id)webView;
    //注册JS调用OC的函数名,handler是JS给的回调
    - (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;
    //移除JS与OC交互的函数名
    - (void)removeHandler:(NSString*)handlerName;
    //添加OC调用JS的函数名,无参数无回调
    - (void)callHandler:(NSString*)handlerName;
    //添加OC调用JS的函数名,有参数无回调
    - (void)callHandler:(NSString*)handlerName data:(id)data;
    //添加OC调用JS的函数名,有参数有回调
    - (void)callHandler:(NSString*)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback;
    //若在WKWebView的VC中使用WKWebview的代理,就实现这个方法
    - (void)setWebViewDelegate:(id)webViewDelegate;
    
    WebViewJavascriptBridge的基本使用
    • 在OC中使用

    1、先为WKWebView添加与JS沟通的bridge桥梁

    self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.wkWebView];
    //如果要在ViewController中实现代理,就设置这个方法
    [self.bridge setWebViewDelegate:self];
    

    2、JS调用OC
    注册需要JS调用OC的函数名,handler是JS给的回调

    //JS第一个按钮点击事件
        [self.bridge registerHandler:@"firstClick" handler:^(id data, WVJBResponseCallback responseCallback) {
            //handler在主线程
            NSLog(@"thread = %@", [NSThread currentThread]);
            __strong typeof(self) strongSelf = weakSelf;
            [strongSelf firstClick:[data valueForKey:@"token"]];
            responseCallback([NSString stringWithFormat:@"成功调用OC的%@方法", [data valueForKey:@"action"]]);
        }];
    

    需要注意的是
    1)这个handler回调是在主线程
    2)循环引用

    3、OC调用JS
    添加OC调用JS的函数名,有参数有回调

    [self.bridge callHandler:@"showTextOnDiv" data:@"这是OC调用JS方法" responseCallback:^(id responseData) {
        NSLog(@"JS给的回调responseData = %@", responseData);
    }];
    
    • 在JS中使用
      在JS中使用需要将这个函数复制到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 = 'wvjbscheme://__BRIDGE_LOADED__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() {document.documentElement.removeChild(WVJBIframe)}, 0);
    }
    

    调用setupWebViewJavascriptBridge函数,在这里面注册被OC调用的方法。

    setupWebViewJavascriptBridge(function(bridge) {
            //注册让OC调用JS的方法,data是OC传递过来的参数,responseHandler是给OC的回调
            bridge.registerHandler("showTextOnDiv", function(data, responseHandler) {
                var textDiv = document.getElementById("setText");
                textDiv.innerHTML = data;
                responseHandler("JS已经干完活啦");//给OC回调一个字符串
            });
    
            bridge.registerHandler("showImageOnDiv", function(data, responseHandler) {
                showImageOnDiv(data);
            });
    });
    

    JS调用OC的方式如下所示

    //JS调用OC
    function firstClick() {
            var action = "firstClick";
            var token = getText(0);
            var paras = getParams(action, token);
            //JS调用OC
            WebViewJavascriptBridge.callHandler("firstClick", paras, function(response) {
                //这里是JS调用OC后,OC给的回调
                var textDiv = document.getElementById("setText");
                textDiv.innerHTML = response;
            });
    }
    

    WebViewJavaScriptBridge源码解析

    WebViewJavaScriptBridge的各个类负责的工作

    WebViewJavascriptBridgeBase:主要包括对bridge的处理,以及OC端消息收发的处理。
    WKWebViewJavascriptBridge:封装了一层WKWebView并实现了代理,代理主要负责拦截符合自己定制的规则的url,并通过WebViewJavascriptBridgeBase处理一些细节。
    WebViewJavascriptBridge:这主要是给UIWebview用的,和WKWebViewJavascriptBridge的逻辑差不多。
    WebViewJavascriptBridge_JS:这个类是向JS中注入代码,负责JS端的消息处理和收发的工作。

    JS调用OC步骤

    1、WebViewJavascriptBridge对象调用callHandler方法,这个方法我们可以从WebViewJavascriptBridge_JS文件中找到

    //JS文件中调用
    WebViewJavascriptBridge.callHandler("firstClick", paras, function(response) {
            //这里是JS调用OC后,OC给的回调
            var textDiv = document.getElementById("setText");
            textDiv.innerHTML = response;
    });
    
    //WebViewJavascriptBridge_JS文件中JS调用OC方法的函数
    function callHandler(handlerName, data, responseCallback) {
        if (arguments.length == 2 && typeof data == 'function') {
            responseCallback = data;
            data = null;
        }
    //发送消息
        _doSend({ handlerName:handlerName, data:data }, responseCallback);
    }
    

    从上述代码中可以看到callHandler传了三个参数handlerName(OC的方法名)、data(参数)、responseCallback(OC接收到消息后给的回调)。
    2、doSend函数进一步处理后发送消息给OC

    function _doSend(message, responseCallback) {
        if (responseCallback) {
              var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
              responseCallbacks[callbackId] = responseCallback;
              message['callbackId'] = callbackId;
         }
        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }
    

    从代码中可以看到,WebViewJavascriptBridge_JS把这个消息的message存储到sendMessageQueue中了,再通过iframe的src读取Url,然后OC通过代理decidePolicyForNavigationAction拦截Url。
    为什么要存储到sendMessageQueue中呢?我们接着看。

    3、代理拦截JS调用的Url

    - (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
        if (webView != _webView) { return; }
        NSURL *url = navigationAction.request.URL;
        __strong typeof(_webViewDelegate) strongDelegate = _webViewDelegate;
    
        if ([_base isWebViewJavascriptBridgeURL:url]) {
            if ([_base isBridgeLoadedURL:url]) {
                [_base injectJavascriptFile];
            } else if ([_base isQueueMessageURL:url]) {//
                [self WKFlushMessageQueue];
            } else {
                [_base logUnkownMessage:url];
            }
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
        
        if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:decidePolicyForNavigationAction:decisionHandler:)]) {
            [_webViewDelegate webView:webView decidePolicyForNavigationAction:navigationAction decisionHandler:decisionHandler];
        } else {
            decisionHandler(WKNavigationActionPolicyAllow);
        }
    }
    

    我们在这里打断点,看到Url是这个样子

    "https://__wvjb_queue_message__/"
    

    所以这个Url只是告诉OC:“JS要调用OC的方法啦”。接下来调用WKFlushMessageQueue方法,
    4、从sendMessageQueue中把JS调用的OC方法message取出来。

    - (void)WKFlushMessageQueue {
        [_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];
        }];
    }
    

    我们看到这里使用evaluateJavaScript方法调用了JS,那这个JS字符串([_base webViewJavascriptFetchQueyCommand])是什么呢?

    - (NSString *)webViewJavascriptFetchQueyCommand {
        return @"WebViewJavascriptBridge._fetchQueue();";
    }
    

    我们再到WebViewJavascriptBridge_JS文件中找到_fetchQueue函数

    /*
    *把JS的消息数组转换成JSON字符串
    */
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }
    

    可以看到,这里做的是从sendMessageQueue中把JS调用的OC方法message取出来的操作,我们现在就明白了,原来JS调用OC方法拦截的Url只是JS想告诉OC:“我有个方法要调用,来我这取一下”,取的message就像下面的JSON字符串。

    [{"handlerName":"firstClick",
    "data":{"action":"firstClick","token":"今晚打老虎"},
    "callbackId":"cb_3_1557740589053"}]
    

    我们在拿到调用消息message后,接下来就是对message的处理。
    5、flushMessageQueue处理消息并调用handler完成OCregisterHandler:handler的回调

    [self.bridge registerHandler:@"callOCToCallJSClick" handler:^(id data, WVJBResponseCallback responseCallback) {
     }];
    

    消息的处理过程如下,对理解原理没什么用的都剔除掉了

    - (void)flushMessageQueue:(NSString *)messageQueueString{
        //将调用的message消息列表的JSON字符串转换成数组
        id messages = [self _deserializeMessageJSON:messageQueueString];
        for (WVJBMessage* message in messages) {
            /*
             回收消息,message格式如下
             {"callbackId":"cb_3_1557740589053",
             "handlerName":"firstClick",
             "data":{"action":"firstClick","token":"今晚打老虎"},
             }
              应答消息,格式如下
             {"responseId":"objc_cb_2",
             "handlerName":"showTextOnDiv",
             "responseData":"JS已经干完活啦"}
             */
            NSString* responseId = message[@"responseId"];
            if (responseId) {
                WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
                responseCallback(message[@"responseData"]);
                [self.responseCallbacks removeObjectForKey:responseId];
            } else {
                WVJBResponseCallback responseCallback = NULL;
                NSString* callbackId = message[@"callbackId"];
                if (callbackId) {
                    responseCallback = ^(id responseData) {
                        if (responseData == nil) {
                            responseData = [NSNull null];
                        }
                        
                        WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                        [self _queueMessage:msg];
                    };
                } else {
                    responseCallback = ^(id ignoreResponseData) {
                        // Do nothing
                    };
                }
                
                WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
                
                //调用block,此时OC的这个回调就会收到JS的调用信息
                /*
                 [self.bridge registerHandler:@"callOCToCallJSClick" handler:^(id data, WVJBResponseCallback responseCallback) {
                 }];
                 */
                handler(message[@"data"], responseCallback);
            }
        }
    }
    

    这一坨代码主要做的就是从全局的messageHandlers中获取OC注册方法时的block回调,如下面代码所示

    WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];
                
    //调用block,此时OC的这个回调就会收到JS的调用信息
    /*
     [self.bridge registerHandler:@"callOCToCallJSClick" handler:^(id data, WVJBResponseCallback responseCallback) {
    }];
    */
    handler(message[@"data"], responseCallback);
    

    responseIdif条件是判断当前message是应答消息(response)还是callback回收消息,这里解释一下,回调消息是指JS(OC)调用OC(JS)成功后,OC(JS)给的回应message主动消息是指JS(OC)调用OC(JS)时发送的消息message

    主动消息,message格式如下
    {"callbackId":"cb_3_1557740589053",
    "handlerName":"firstClick",
    "data":{"action":"firstClick","token":"今晚打老虎"},
    }
    回调消息,格式如下
    {"responseId":"objc_cb_2",
    "handlerName":"showTextOnDiv",
    "responseData":"JS已经干完活啦"}

    我们现在研究的是JS调用OC,所以先看else部分

    WVJBResponseCallback responseCallback = NULL;
    NSString* callbackId = message[@"callbackId"];
    if (callbackId) {
        responseCallback = ^(id responseData) {
            if (responseData == nil) {
                 responseData = [NSNull null];
            }
                        
             WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
             [self _queueMessage:msg];
        };
    } else {
        responseCallback = ^(id ignoreResponseData) {
            // Do nothing
        };
    }
    

    从代码可知,如果OC在接收到JS的回调后,又返回了一个回调给JS,比如下面这样

    [self.bridge registerHandler:@"firstClick" handler:^(id data, WVJBResponseCallback responseCallback) {
        responseCallback([NSString stringWithFormat:@"成功调用OC的%@方法", [data valueForKey:@"action"]]);
    }];
    

    那么这里就会调用_queueMessage方法,目的是调用evaluateJavascript给JS传值

    - (void)_queueMessage:(WVJBMessage*)message {
            [self _dispatchMessage:message];
    }
    
    - (void)_dispatchMessage:(WVJBMessage*)message {
        NSString *messageJSON = [self _serializeMessage:message pretty:NO];
        NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
        if ([[NSThread currentThread] isMainThread]) {
            [self _evaluateJavascript:javascriptCommand];
        } else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self _evaluateJavascript:javascriptCommand];
            });
        }
    }
    

    我们看到,如果在JS调用OC成功以后,OC给了JS回调,那么这里就会多一步操作,执行WebViewJavascriptBridge._handleMessageFromObjC('%@');这样一段JS将回调的值传回JS,同理,我们到WebViewJavascriptBridge_JS文件中找到_handleMessageFromObjC函数,

    //把消息发送给JS
        function _handleMessageFromObjC(messageJSON) {
            _dispatchMessageFromObjC(messageJSON);
        }
         /*
         *处理OC调用JS的消息
         *messageJSON:{\"responseId\":\"cb_1_1557747121840\",\"responseData\":\"成功调用OC的firstClick方法\"}
         */
        function _dispatchMessageFromObjC(messageJSON) {
                var message = JSON.parse(messageJSON);
                var messageHandler;
                var responseCallback;
    
                if (message.responseId) {
                    responseCallback = responseCallbacks[message.responseId];
                    if (!responseCallback) {
                        return;
                    }
                    responseCallback(message.responseData);
                    delete responseCallbacks[message.responseId];
                } else {
                    if (message.callbackId) {
                        var callbackResponseId = message.callbackId;
                        responseCallback = function(responseData) {
                            _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        };
                    }
                    
                    var handler = messageHandlers[message.handlerName];
                    if (!handler) {
                        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                    } else {
                        handler(message.data, responseCallback);
                    }
                }
        }
    

    从上面的代码可知,通过responseCallback将消息回调给了JS。
    JS调用OC的总结如下:
    1、WebViewJavascriptBridge对象调用callHandler函数
    2、doSend函数存储messagesendMessageQueue中,并设置iFrame读取Url
    3、OC的WebView拦截Url,通过evaluateJavaScriptsendMessageQueue中获取message
    4、把message中的data数据和回调,一起回调给OC
    5、OC在收到JS调用的信息后,给予JS回调WebViewJavascriptBridge._handleMessageFromObjC('%@');

    WebViewJavaScriptBridge--JS调OC.png
    OC调用JS步骤

    1、bridge调用callHandler:data:responseCallback方法,分别传入调用的JS函数名、参数、回调,如下所示

    [self.bridge callHandler:@"showTextOnDiv" data:@"这是OC调用JS方法" responseCallback:^(id responseData) {
        NSLog(@"showTextOnDiv的回调responseData = %@", responseData);
    }];
    

    2、调用WebViewJavascriptBridgeBasesendData:responseCallback:handlerName方法,以时间戳和_uniqueId生成callbackId,并把callbackId、参数、函数名转换成JSON字符串发送给JS。

    - (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
        NSMutableDictionary* message = [NSMutableDictionary dictionary];
        
        if (data) {
            message[@"data"] = data;
        }
        
        if (responseCallback) {
            NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
            self.responseCallbacks[callbackId] = [responseCallback copy];
            message[@"callbackId"] = callbackId;
        }
        
        if (handlerName) {
            message[@"handlerName"] = handlerName;
        }
        [self _queueMessage:message];
    }
    
    - (void)_dispatchMessage:(WVJBMessage*)message {
        NSString *messageJSON = [self _serializeMessage:message pretty:NO];
        NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
        if ([[NSThread currentThread] isMainThread]) {
            [self _evaluateJavascript:javascriptCommand];
        } else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self _evaluateJavascript:javascriptCommand];
            });
        }
    }
    

    我们看到发送消息sendData方法实际上是调用了WebViewJavascriptBridge_JS文件中的_handleMessageFromObjC()函数。
    3、调用_handleMessageFromObjC()处理消息,并调用handler()把消息回调给JS

    //把消息发送给JS
        function _handleMessageFromObjC(messageJSON) {
            _dispatchMessageFromObjC(messageJSON);
        }
         /*
         *处理OC调用JS的消息
         *messageJSON:{\"responseId\":\"cb_1_1557747121840\",\"responseData\":\"成功调用OC的firstClick方法\"}
         */
        function _dispatchMessageFromObjC(messageJSON) {
                var message = JSON.parse(messageJSON);
                var messageHandler;
                var responseCallback;
    
                if (message.responseId) {
                    responseCallback = responseCallbacks[message.responseId];
                    if (!responseCallback) {
                        return;
                    }
                    responseCallback(message.responseData);
                    delete responseCallbacks[message.responseId];
                } else {
                    if (message.callbackId) {
                        var callbackResponseId = message.callbackId;
                        responseCallback = function(responseData) {
                            _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                        };
                    }
                    
                    var handler = messageHandlers[message.handlerName];
                    if (!handler) {
                        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                    } else {
                        handler(message.data, responseCallback);
                    }
                }
        }
    

    _dispatchMessageFromObjC()函数中,因为我们发送的是OC调用JS的回收消息callback,所以执行的是else代码块,如下所示

    if (message.callbackId) {
        var callbackResponseId = message.callbackId;
        responseCallback = function(responseData) {
        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
        };
    }
    var handler = messageHandlers[message.handlerName];
    if (!handler) {
        console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
    } else {
        handler(message.data, responseCallback);
    }
    

    这一段的目的就是调用handler(message.data, responseCallback);,把调用参数和回调传递给JS,进入JS的回调,进入下面代码中的function里。

    bridge.registerHandler("showTextOnDiv", function(data, responseHandler) {
        var textDiv = document.getElementById("setText");
        textDiv.innerHTML = data;
        responseHandler("JS已经干完活啦");//给OC回调一个字符串
    });
    

    if代码块里,如果JS的函数给了OC回应的话,那么会调用_doSend()函数将消息发送给OC,流程和JS调用OC时一样,这里就不啰嗦了。
    OC调用JS总结:
    1、OC通过调用jsBridge对象callHandler,实现JS调用
    2、再通过base的sendData:responseCallback:handlerName方法发送调用的消息给JS
    3、JS调用_handleMessageFromObjC()处理消息,并通过handler把OC的呼叫告诉JS。

    全文总结

    现在为止,已经把WebViewJavascriptBridge框架走完了,现精简总结如下:
    1、OC和JS都维护了一个JSBridge对象作为桥梁实现交互,一个是WebViewJavascriptBridge_JS文件,一个是WKWebViewJavascriptBridge文件。
    2、JS调用OC是通过iFrame拦截Url,再从JS的sendMessageQueue中获取函数消息,最后把函数的消息回调给OC的registerHandler
    3、OC调用JS实际上就是通过evaluateJavaScript把函数消息给_handleMessageFromObjC函数,_handleMessageFromObjC再把函数消息发送给JS的registerHandler

    Demo地址

    相关文章

      网友评论

        本文标题:iOS OC与JS交互(四)-- WebViewJavascri

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