美文网首页WebViewios实用开发技巧IOS
OC和JS交互(UIWebView)中级篇3

OC和JS交互(UIWebView)中级篇3

作者: bigParis | 来源:发表于2017-11-24 11:19 被阅读29次

    上一篇博文重点讲了下我们项目中最常用的JS调用OC, 花开两朵各表一枝, 本文将重点讲下OC调用JS.

    OC调用JS的入口在VC, 下面是代码

    [self.bridge callHandler:@"getUserInfo" data:@{@"userId":@"DX001"} responseCallback:^(id responseData) {
            NSString *userInfo = [NSString stringWithFormat:@"%@,姓名:%@,年龄:%@", responseData[@"userID"], responseData[@"userName"], responseData[@"age"]];
            UIAlertController *vc = [UIAlertController alertControllerWithTitle:@"从网页端获取的用户信息" message:userInfo preferredStyle:UIAlertControllerStyleAlert];
            UIAlertAction *cancelAction = [UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:nil];
            UIAlertAction *okAction = [UIAlertAction actionWithTitle:@"好的" style:UIAlertActionStyleDefault handler:nil];
            [vc addAction:cancelAction];
            [vc addAction:okAction];
            [self presentViewController:vc animated:YES completion:nil];
        }];
    
    WebViewJavascriptBridge.m
    - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
        [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
    }
    
    WebViewJavascriptBridgeBase.m
    - (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];
    }
    

    最终来到了WebViewJavascriptBridgeBasesendData方法里面, 这里创建一个NSMutableDictionary对象message, 并把VC传递进来的参数data = @{@"userId":@"DX001"}, handlerName = @"getUserInfo"还有responseCallback保存起来, 和之前JS保存responseCallback方法相似, 这里也是生成一个callbackId, 并把responseCallback保存在以callbackId为key的字典self.responseCallbacks中, 最后执行[self _queueMessage:message];

    WebViewJavascriptBridgeBase.h
    @interface WebViewJavascriptBridgeBase : NSObject
    
    @property (strong, nonatomic) NSMutableArray* startupMessageQueue;
    
    WebViewJavascriptBridgeBase.m
    - (void)_queueMessage:(WVJBMessage*)message {
        if (self.startupMessageQueue) {
            [self.startupMessageQueue addObject:message];
        } else {
            [self _dispatchMessage:message];
        }
    }
    

    我们来回忆下上文是怎么使用_dispatchMessage

    responseCallback = ^(id responseData) {
      if (responseData == nil) {
          responseData = [NSNull null];
      }
                        
      WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
      [self _queueMessage:msg];
    };
    

    这里是在OC的block中执行了_queueMessage, 实际也是OC调用JS. 只是在上文中, OC调用JS不是重点. 好了这里也顺便分析下我们之前遗留下来的问题:startupMessageQueue是干什么的?

    WebViewJavascriptBridgeBase.m
    -(id)init {
        self = [super init];
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
        return(self);
    }
    
    - (void)dealloc {
        self.startupMessageQueue = nil;
        self.responseCallbacks = nil;
        self.messageHandlers = nil;
    }
    
    - (void)reset {
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    
    - (void)injectJavascriptFile {
        NSString *js = WebViewJavascriptBridge_js();
        [self _evaluateJavascript:js];
        if (self.startupMessageQueue) {
            NSArray* queue = self.startupMessageQueue;
            self.startupMessageQueue = nil;
            for (id queuedMessage in queue) {
                [self _dispatchMessage:queuedMessage];
            }
        }
    }
    
    - (void)_queueMessage:(WVJBMessage*)message {
        if (self.startupMessageQueue) {
            [self.startupMessageQueue addObject:message];
        } else {
            [self _dispatchMessage:message];
        }
    }
    

    startupMessageQueueWebViewJavascriptBridgeBase中的一个数组, 这个数组在WebViewJavascriptBridgeBase初始化的时候被创建, 但是只是一个空的数组, 并且在初始化注入的时候就被取出来并置空了, 所以后面正常情况是不存在_queueMessage走进if分支的, 只有一种情况, 就是在injectJavascriptFile还没执行的时候, 先进行了OC对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)
            }
    

    这段代码写进html的时候应该是不存在的, 因为在网页加载的时候OC就完成了注入, 但是如果上面的这段代码如果不在html中, 那还是有可能的, 而且在实际开发中, 难道我们还要要求前端的同事每个网页都加上面的一段代码, 也是不现实的. 所以作者应该是通过综合的考虑才加入了startupMessageQueue的, 好了, 在本例中, startupMessageQueue还是没有实际作用, 代码最终回到上文的后半部分, OC回调JS, 这里要注意一下的问题就是, 运行JS脚本可能会存在线程安全的问题, 所以, 一定要在主线程执行JS

    if ([[NSThread currentThread] isMainThread]) {
            [self _evaluateJavascript:javascriptCommand];
    
        } else {
            dispatch_sync(dispatch_get_main_queue(), ^{
                [self _evaluateJavascript:javascriptCommand];
            });
        }
    

    经过一些列调用_queueMessage->_dispatchMessage->_evaluateJavascript->_handleMessageFromObjC->_dispatchMessageFromObjC->_doDispatchMessageFromObjC, 好了, 还是来到下面这段代码

    function _doDispatchMessageFromObjC() {
        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);
            }
        }
    }
    

    我们在Safari中下断点, 发现


    image.png

    , 这里有个小的tips, 因为这段脚本不在html的页面里面不能直接打断点, 要先在

    bridge.registerHandler('getUserInfo', function(data, responseCallback) {
        console.log("OC中传递过来的参数:", data);
        // 把处理好的结果返回给OC
        responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
    });
    

    responseCallback这里打断点, 然后调用JS接口, Safari左侧会出现调用堆栈, 里面有我们注入的代码, 这时候就可以在_dispatchMessageFromObjC里面打断点了, 好了, 继续分析_dispatchMessageFromObjC里面的代码.
    由于调用的时候并没有给responseId赋值, 所以, 代码走到

    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);
    }               
    

    这里是和之前JS调用OC时候, OC回调JS不同的, 这里message.responseId是没有值的, message.callbackId是有值的, 所以会在这里创建一个JS的responseCallback, 后面取出handler并调用handler(message.data, responseCallback);, 下面来看下messageHandlers吧, 实际和OC注册很相似

    function registerHandler(handlerName, handler) {
            messageHandlers[handlerName] = handler;
        }
    

    还没完, 这里OC调用JS的时候也传递了一个block, 这个block最终传递到了JS

    bridge.registerHandler('getUserInfo', function(data, responseCallback) {
        console.log("OC中传递过来的参数:", data);
        // 把处理好的结果返回给OC
        responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
    });       
    

    把函数名作为key, 回调方法作为value, 建立messageHandlers字典, 所以最终执行handler(message.data, responseCallback);实际是调用了

    bridge.registerHandler('getUserInfo', function(data, responseCallback) {
        console.log("OC中传递过来的参数:", data);
        // 把处理好的结果返回给OC
        responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})
    });      
    

    中函数体里的代码, 如果没有OC传递的block, OC调用JS就到此结束了, 输出console.log("OC中传递过来的参数:", data);完成调用, 但是OC传递了block, 所以还要继续分析, responseCallback是在刚才通过JS代码创建的回调, 只有OC传递了block才会创建.

    responseCallback = function(responseData) {
        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
    };
    

    在JS中调用responseCallback({"userID":"DX001", "userName":"旋之华", "age":"18", "otherName":"旋之华"})实际会来到_doSend, responseData正是JS中传递来的参数

    image.png , 而handlerNameresponseId都是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;
    }
    

    进入_doSend, 由于没传递responseCallback, 所以if走不到, 这里还是把OC传递过来的message保存在sendMessageQueue中, 然后改变src触发OC执行, 来到OC的

    - (void)flushMessageQueue:(NSString *)messageQueueString{
        if (messageQueueString == nil || messageQueueString.length == 0) {
            NSLog(@"WebViewJavascriptBridge: WARNING: ObjC got nil while fetching the message queue JSON from webview. This can happen if the WebViewJavascriptBridge JS is not currently present in the webview, e.g if the webview just loaded a new page.");
            return;
        }
    
        id messages = [self _deserializeMessageJSON:messageQueueString];
        for (WVJBMessage* message in messages) {
            if (![message isKindOfClass:[WVJBMessage class]]) {
                NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
                continue;
            }
            [self _log:@"RCVD" json:message];
            
            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"]];
                
                if (!handler) {
                    NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                    continue;
                }
                
                handler(message[@"data"], responseCallback);
            }
        }
    }
    

    由于responseId有值, 而从_responseCallbacks取出来的responseCallback正是OC之前传入的block, 所以下面的代码

    WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
    responseCallback(message[@"responseData"]);
    [self.responseCallbacks removeObjectForKey:responseId];
    

    执行responseCallback, 回调OC之前传递的block, 至此, OC调用JS, 并把JS端数据取回完美结束.

    总结

    在实际中, 我们应该很少会用到JS提供接口给OC调用, 通常是OC提供稳定通用接口给JS调用, 所以本文不是我们实践的重点, 但是作为讲解框架的完整性, 我们应该把OC调用JS和JS调用OC都进行详细的分析, 这样能更好的理解作者设计的意图和架构的巧妙之处.

    遗留问题

    1 难道每个Web页面都要加入setupWebViewJavascriptBridge这段代码, 这应该是所有开发者都不能接受的.
    2 对于WKWebView怎么处理? 也能拦截吗?
    3 如何设计一个通用的WebView或者WebViewController?

    下面是本文用到的代码的github, 不是小编原著.
    WebViewJSBridgeDemo

    相关文章

      网友评论

        本文标题:OC和JS交互(UIWebView)中级篇3

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