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

本篇笔记记录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地址

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

推荐阅读更多精彩内容