Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge

图片来源于网络(侵删).jpg
WebViewJavaScriptBridge是被使用最多,也是最好用的移动端与JS交互的第三方框架,在此总结一下它的使用方法和内部原理。

一.使用方法
iOS端(这里以UIWebView为例)

1.导入头文件

#import "WebViewJavascriptBridge.h"

2.声明桥接对象

@property WebViewJavascriptBridge* bridge;

3.初始化桥接对象

self.bridge = [WebViewJavascriptBridge bridgeForWebView:webView];

4.注册函数

[self.bridge registerHandler:@"getUserInfo" handler:^(id data, WVJBResponseCallback responseCallback) {

        NSDictionary *dict = @{
                                    @"userid":@"0001"
                                    };
        
        responseCallback(dict);
    }];

@"getUserInfo":与JS端共同商定的函数名,JS在调用时函数名必须一致
data:函数调用时传递过来的数据
responseCallback:函数被调用时传递过来的回调函数

4.调用函数

[self.javaScriptBridge callHandler:@"webViewRefresh" data:nil responseCallback:^(id responseData) {
       
 }];

@"webViewRefresh":与JS端共同商定的函数名,JS在注册时函数名必须一致
data:调用函数时传递过去的数据
responseCallback:调用函数时传递过去的回调函数
responseData:传递过去的回调函数被JS调用时所传递过来的数据
JS端

1.声明一个函数,将此函数复制到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)
}

2.注册或者调用交互方法

setupWebViewJavascriptBridge(function(bridge) {
    
    /* Initialize your app here */

    bridge.registerHandler('JS Echo', function(data, responseCallback) {
        console.log("JS Echo called with:", data)
        responseCallback(data)
    })
    bridge.callHandler('ObjC Echo', {'key':'value'}, function responseCallback(responseData) {
        console.log("JS received response:", responseData)
    })
})
二.内部原理

1.框架文件

Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge_第1张图片
20.pic.jpg

① WebViewJavascriptBridgeBase中声明和实现了基础的方法和属性
② WebViewJavascriptBridge_js中是一段JS代码,在初始化时编译,用于交互
③ WebViewJavascriptBridge和WKWebViewJavascriptBridge是分别适应UIWebView和WKWebView的继承于WebViewJavascriptBridgeBase的子类
2.初始化

Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge_第2张图片
1.pic.jpg

Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge_第3张图片
2.pic.jpg

在JS文件被加载时会执行setupWebViewJavascriptBridge的方法,改变iframe.src,就会发起页面请求,从而被UIWebView的代理方法拦截到,执行injectJavascriptFile,编译WebViewJavascriptBridge_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)
}

3.JS调用OC函数原理(这里以UIWebView为例)
首先需要OC注册函数

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

OC注册函数需要调用WebViewJavascriptBridge的方法

- (void)registerHandler:(NSString*)handlerName handler:(WVJBHandler)handler;

那么看一下它的做了什么

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}

它将handlerName为key,以handler为value储存在一个叫做messageHandlers的字典中,注册就完成了
然后需要JS调用OC注册好的函数

bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS got response', response)
            })

JS调用函数需要用到WebViewJavascriptBridge_js的方法

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 = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

这个方法将handlerName, data, responseCallback储存在message字典中,并将message字典储存在sendMessageQueue数组中,最后改变了iframe.src,发送请求

Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge_第4张图片
3.pic.jpg
- (NSString *)webViewJavascriptFetchQueyCommand {
    return @"WebViewJavascriptBridge._fetchQueue();";
}

被被UIWebView的代理方法拦截到之后首先执行JS方法WebViewJavascriptBridge._fetchQueue(),拿到要调用的函数的信息

function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        return messageQueueString;
    }

然后执行OC的flushMessageQueue方法

- (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) {//当OC调用JS注册的函数后执行回调方法时才有responseId,此处后面会介绍
            WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
            responseCallback(message[@"responseData"]);
            [self.responseCallbacks removeObjectForKey:responseId];
        } else {//当JS调用OC注册的函数
            WVJBResponseCallback responseCallback = NULL;
            NSString* callbackId = message[@"callbackId"];
            if (callbackId) {//如果有JS的回调
                responseCallback = ^(id responseData) {
                    if (responseData == nil) {
                        responseData = [NSNull null];
                    }
                    
                   //注意此处添加了responseId,之后在WebViewJavascriptBridge_js的_dispatchMessageFromObjC方法中会用到
                    WVJBMessage* msg = @{ @"responseId":callbackId, @"responseData":responseData };
                    [self _queueMessage:msg];//执行JS回调函数
                };
            } else {
                responseCallback = ^(id ignoreResponseData) {
                    // Do nothing
                };
            }
            
            WVJBHandler handler = self.messageHandlers[message[@"handlerName"]];//根据函数名拿到handler
            
            if (!handler) {
                NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message);
                continue;
            }
            
            handler(message[@"data"], responseCallback);//执行handler
        }
    }
}

下面我们再看一下JS的回调函数是如何被执行的

- (void)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];

    //执行JS的_handleMessageFromObjC拿到要执行的回调函数
    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的handleMessageFromObjC方法是如何找到相应的回调函数的

function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _doDispatchMessageFromObjC();
        }
        
        function _doDispatchMessageFromObjC() {
            var message = JSON.parse(messageJSON);
            var messageHandler;
            var responseCallback;

            if (message.responseId) {
//之前OC的flushMessageQueue方法中已经添加了responseId,那么进到这个判断,根据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);
                }
            }
        }
    }

至此,JS调用OC注册的函数流程完结,难点在于responseCallback如何执行,要记住responseCallback是由调用方传入的参数,应由调用方执行。

4.OC调用JS函数原理(这里以UIWebView为例)
首先需要JS注册函数

bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) {
            log('ObjC called testJavascriptHandler with', data)
            var responseData = { 'Javascript Says':'Right back atcha!' }
            log('JS responding with', responseData)
            responseCallback(responseData)
        })

看一下这个方法的实现,在WebViewJavascriptBridge_js中

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

可以看到这里和OC注册函数时的操作一样,都是将handlerName为key,以handler为value储存在一个叫做messageHandlers的字典中,注册就完成了

然后就是OC开始调用这个函数

[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];

看一下这个方法的实现,在WebViewJavascriptBridge中

- (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback {
    [_base sendData:data responseCallback:responseCallback handlerName:handlerName];
}

- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
    NSMutableDictionary* message = [NSMutableDictionary dictionary];
    
    if (data) {
        message[@"data"] = data;
    }
    
    if (responseCallback) {//注意这里,如果有responseCallback,就会callbackId为key存起来
        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)_queueMessage:(WVJBMessage*)message {
    if (self.startupMessageQueue) {
        [self.startupMessageQueue addObject:message];
    } else {
        [self _dispatchMessage:message];
    }
}

- (void)_dispatchMessage:(WVJBMessage*)message {
    NSString *messageJSON = [self _serializeMessage:message pretty:NO];
    [self _log:@"SEND" json:messageJSON];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\\" withString:@"\\\\"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\"" withString:@"\\\""];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\'" withString:@"\\\'"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\n" withString:@"\\n"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\r" withString:@"\\r"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\f" withString:@"\\f"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2028" withString:@"\\u2028"];
    messageJSON = [messageJSON stringByReplacingOccurrencesOfString:@"\u2029" withString:@"\\u2029"];
    
    NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
    if ([[NSThread currentThread] isMainThread]) {
        [self _evaluateJavascript:javascriptCommand];

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

可见最终还是执行到WebViewJavascriptBridge_js中的handleMessageFromObjC方法才能拿到要执行的JS代码,那我们看一下这里跟JS调用OC函数不同在哪里

function _dispatchMessageFromObjC(messageJSON) {
        if (dispatchMessagesWithTimeoutSafety) {
            setTimeout(_doDispatchMessageFromObjC);
        } else {
             _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) {//如果有responseCallback,注意之前的处理
                    var callbackResponseId = message.callbackId;
//注意此处它为responseCallback赋值了一个函数,当执行responseCallback时会执行_doSend方法,并且以responseId为key传入了callbackResponseId
                    responseCallback = function(responseData) {
                        _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                    };
                }
                
               //找到相应的handler并执行
                var handler = messageHandlers[message.handlerName];
                if (!handler) {
                    console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
                } else {
                    handler(message.data, responseCallback);
                }
            }
        }
    }

此处关键在于responseCallback是OC传入的代码块,应该由OC执行,所以JS调用_doSend方法改变iframe.src发起请求,通知OC执行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 = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

然后OC拦截到请求的操作之前已经过了一遍,那不同的地方在哪呢,那就是有了responseId

- (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) {//有了responseId,这很关键,根据responseId找到responseCallback并执行
            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);
        }
    }
}

找到了之前存储的responseCallback并执行,到此OC调用JS注册的函数流程完毕。

三.总结

受限于JS知识水平不足,有些地方分析的不太好,不对之处还望指正,不胜感激!另外我还写了一篇JavaScriptCore的使用介绍,JavaScriptCore是苹果提供的一套用于与JS交互的框架,感兴趣的朋友可以点这里看一下

你可能感兴趣的:(Hybrid~iOS与JS交互那点事儿之WebViewJavaScriptBridge)