iOS源码补完计划-WebViewJavascriptBridg
提及其原理、所有用过它的童鞋都会说他在js和Native(原生)之间搭建了一个桥梁。通过这个桥、使他们相互通信。但具体怎么通信呢?这个桥如何工作?十有八九说却不清。
JSBridge的逻辑简而言之如下
-
我这人比较喜欢先贴结论、方便伸手党(像我这种)。
oc和js相互调用的逻辑都是如此
(请忽略脑图的指向、能连上就行。由左右向中间)
JS调用Native
- 调用方时会生成一个id。
- 将调用的callback与id(callbackId)绑定备用。
- 再将方法名与id(callbackId)发给注册方。
- 注册方通过方法名、找出对应的响应方法(handler)。
- handler执行完毕后通过id(responseId)找出调用方对应的callback返回。
- responseId代表被调用方发起、callbackId代表调用方发起、值都相同。
注意有一点不同
- js是通过重定向通知oc处理逻辑。参数先存在js中、然后通过oc调用js中_fetchQueue方法被oc获取。
- oc是通过直接调用_handleMessageFromObjC并且传递了参数通知js处理逻辑。
正文
WebViewJavascriptBridge的原理本质上也是协议拦截。
- 这个库、具体的用法我就不写了、反正写也是copy别的教学帖子。
而且、在JSCore以及WKWebView已经极其成熟的当下。WebViewJavascriptBridge用到的地方并不是那么多。 - 我比较关心的是他如何以注册以及调用这种写法来实现的协议拦截。
所以我也并不是每行源码都贴出来、只是贴一些关键的功能性代码 - 其实我以前没用过JSBridge、15年入行的时候就已经是JSCore普及的时代了。
- 从零开始一行一行读、有兴趣不妨一起。
-
随手下了一个最新的、2017-12-19:当前版本号6.0.2。
先看JS调用Native
-
Native中注册:
[self.bridge registerHandler:@"getUserId" handler:^(id data, WVJBResponseCallback responseCallback) { if (responseCallback) { // 反馈给JS responseCallback(@{@"userId": @"123456"}); } }];
没什么问题、方法名、js传进来的ballback(参数、回调block)
继续看.
#import "WebViewJavascriptBridge.h" - (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler { _base.messageHandlers[handlerName] = [handler copy]; }
- _base:WebViewJavascriptBridge所持有的WebViewJavascriptBridgeBase(简称base)对象。
- messageHandlers:字典。存储了注册的方法名、ballback。
然后、线索断了。也就是说、ios这边主动做的事情、已经没了。
就是在注册的时候将方法名、block。存储起来备用。
既然是备用、搜索这个函数messageHandlers、我们可以发现。
- 蓝色部分:WebView已经WKWebview的注册事件、就是上面我们说的那样。
- 绿色部分:看写法就知道是js文件。内部确实也是js端的注册方法。
- 红色部分:没错红色部分就是刚才我们使用的这个字典、具体的用处了。
那继续看红色部分:
- (void)flushMessageQueue:(NSString *)messageQueueString;
#import "WebViewJavascriptBridgeBase.h" - (void)flushMessageQueue:(NSString *)messageQueueString{ //省略掉其他代码之后 ...... WVJBHandler handler = self.messageHandlers[message[@"handlerName"]]; if (!handler) { NSLog(@"WVJBNoHandlerException, No handler for message from JS: %@", message); continue; } handler(message[@"data"], responseCallback); }
- messageQueueString:字符串。本身的格式应该大概是
"[{"handlerName":"getUserId","data":null,"callbackId":"cb_2_1513740848071"}]"
是个字符串形的json、每部含有三个参数。除了callbackId、我们应该都很好理解。
- message[@"data"]: 我们注册时候的参数。
- responseCallback:显而易见是我们注册时候的回调函数。
那先来看看回调怎么传递给js的吧
//也是在该方法中、生成了这个responseCallback 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 };
在我们触发回调的时候、我们responseCallback(@{@"userId": @"123456"});
其中@{@"userId": @"123456"}。就是这个responseData。
通过与callbackId关联成一个json。调用_queueMessage方法处理。
注意。
这里callbackId已经更名为responseId。
然后会再走入此大方法一次。进入
if (responseId) { WVJBResponseCallback responseCallback = _responseCallbacks[responseId]; responseCallback(message[@"responseData"]); [self.responseCallbacks removeObjectForKey:responseId]; }
然后直接回调给js。这次的回调、才是真正的返还给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:@"\\\\"]; ******对json字符串进行一系列格式化处理***** 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]; }); } }
message:
{"responseId":"cb_3_1513741962583","responseData":{"userId":"123456"}};
javascriptCommand:
WebViewJavascriptBridge._handleMessageFromObjC('{\"responseId\":\"cb_3_1513741962583\",\"responseData\":{\"userId\":\"123456\"}}');
_evaluateJavascript:方法
底层是让webview去注入这段js函数至于_handleMessageFromObjC的实现
就是属于WebViewJavascriptBridge_js文件中的范畴了。一会从js端切入的时候再去看。
所以说这段代码、就是oc返回给js的回调函数无误。
再回过头来看看-(void)flushMessageQueue:(NSString *)messageQueueString;方法是如何被调用的
再次搜索、很明显了、是拦截协议并且判断复合要求之后直接调用的。没什么太绕的东西。
简单的标注了一下
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { if (webView != _webView) { return YES; } NSURL *url = [request URL]; __strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate; if ([_base isWebViewJavascriptBridgeURL:url]) { //js通过Bridge发起的url if ([_base isBridgeLoadedURL:url]) { //注入js(WebViewJavascriptBridge_js) [_base injectJavascriptFile]; } else if ([_base isQueueMessageURL:url]) { //js主动调启oc NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; //去调用刚才分析的那个方法--(通过注册的方法名、调用对应的block) [_base flushMessageQueue:messageQueueString]; } else { //控制台报错 [_base logUnkownMessage:url]; } //拦截 return NO; } else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) { //正常回调给webView的VC return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType]; } else { return YES; } }
至此、OC注册Handler时所做的事、结束。
- OC将方法名、block(参数、回调)储存到字典。
- OC接收到js调用的url、将block取出。传入data/callback并调用该block。
- OC在方法处理完毕时。通过js传入的callbackId、以及我们的返回值作为参数、调用bridgejs文件中的_handleMessageFromObjC方法。将返回值callback给js中的指定ballback。
- 需要注意一点的是、JSBridge在发起请求的时候、并不是将参数、callbackId等直接作为url发送出来。而是直接请求https://wvjb_queue_message/(这一点应该算是其中蛮出彩的地方了。很多人也只是知道其使用的是协议拦截)
- 参数通过bridgejs生成、并且获取。具体这一步如何实现、下面分析js中调用Native的时候再来看(因为现在我也没呢~)。
-
js中调用Native注册的方法:
//app.html bridge.callHandler('getUserId','参数不需要的话可以省略不谢',function(response){ log(response.userId) }) //WebViewJavascriptBridge_JS function callHandler(handlerName, data, responseCallback) { if (arguments.length == 2 && typeof data == 'function') { responseCallback = data; data = null; } _doSend({ handlerName:handlerName, data:data }, responseCallback); }
进行了一些参数处理(js中很多都会根据传入参数数量的不同、内部进行进一步处理)、处理结束直接丢给_doSend函数
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; }
这里我们看到了一个很熟悉的参名字callbackId
- 就是说js的callback函数在这里会被保存起来。以callbackId为键保存在responseCallbacks这个字典中、将来可以根据callbackId获取、完成回调。
- callbackId也作为新的参数、添加进了message字典中。
ok、线索又断了。剩下一个sendMessageQueue以及messagingIframe
- messagingIframe:
这个应该比较容易理解。iframe是一个内嵌的网页标签。你既然修改了对应的src(链接)、webView自然会收到一个重定向的请求。
- sendMessageQueue
既然修改了iframe的src、让webVIew拦截了协议。sendMessageQueue自然就是为了提供参数而存在的了。
具体、我们来找找看(搜索sendMessageQueue)。
//WebViewJavascriptBridge_JS function _fetchQueue() { var messageQueueString = JSON.stringify(sendMessageQueue); sendMessageQueue = []; return messageQueueString; } //#import "WebViewJavascriptBridgeBase.h" - (NSString *)webViewJavascriptFetchQueyCommand { return @"WebViewJavascriptBridge._fetchQueue();"; } - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { ***省略*** //js主动调启oc NSString *messageQueueString = [self _evaluateJavascript:[_base webViewJavascriptFetchQueyCommand]]; //去调用刚才分析的那个方法--(通过注册的方法名、调用对应的block) [_base flushMessageQueue:messageQueueString]; ***省略*** }
- _fetchQueue负责提供刚才封装的Message(含有callbackID那个)
- webViewJavascriptFetchQueyCommand负责在oc中注入js。调用_fetchQueue
- webView重定向时、调用webViewJavascriptFetchQueyCommand获取参数、并且传递给flushMessageQueue去执行oc中注册方法的block。
这不是完事了么...
对啊、这就完事了。注册-调用-回调、一个闭环。具体可以翻回去再看一遍、会恍然大悟。决定画个图。图已经放在最上面了
OC调用JS
先看js文件吧、还是想先从注册看起。
既然我们是iOS开发、js这边就从配置环境开始看代码吧。毕竟很多人还是会很好奇js中的bridge实例从哪来的。
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(function(bridge) {
<!-- 操作bridge -->
}
这段呢、是我从网上copy来的。基本所有教学帖子都这么用。
通过调用setupWebViewJavascriptBridge方法、并传入一个callback函数、来获取bridge对象。
-
啥是callback?
其实就是我们block或者闭包、block本质上也就是个代码块而已。只不过js不爱整那么多花花事罢了。
因为这个bridge对象是在加载完我们iOS本地的bridge_js文件之后才会生成。生成完丢进callBack还给你。
-
何时加载的bridge_js文件?
之前我们分析代码的时候已经提到了、iframe修改src会触发webView的代理。方法中第四行的WVJBIframe对象、就是触发加载bridge_js(wvjbscheme://__ BRIDGE_LOADED __)的iframe。
-
何时返回的bridge对象?
方法中的1-3行。可能看着有点别扭、我们可以调整一下顺序。
if (window.WebViewJavascriptBridge) {
callback(WebViewJavascriptBridge);
return ;
}
if (window.WVJBCallbacks) {
window.WVJBCallbacks.push(callback);
return;
}
window.WVJBCallbacks = [callback];
WebViewJavascriptBridge对象是在bridge_js内部被定义以及实现的。
就是说:
1、如果有WebViewJavascriptBridge直接返回。
2、否则每次调用时将callback放入数组。等生成了bridge、再遍历返回。
初始化就到这、继续看js中注册方法的代码。
bridge.registerHandler('testJavascriptHandler', function(data, responseCallback) { var responseData = { 'Javascript Says':'Right back atcha!' }; if(responseCallback) { responseCallback(responseData); }; });
感觉和oc注册方法的代码一样(其实注册和调用、两端的方法样式都是相同的)。
接着看内部、其实这边的实现逻辑。也和oc一样。
function registerHandler(handlerName, handler) { messageHandlers[handlerName] = handler; }
注册字典@{方法名:handler函数};
搜索messageHandlers
function _dispatchMessageFromObjC(messageJSON) { var handler = messageHandlers[message.handlerName]; if (!handler) { console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message); } else { handler(message.data, responseCallback); } }
- 根据message.handlerName取出对应的handler、然后把responseCallback丢进去执行。
- responseCallback哪来的?和OC中的实现一样
将callbackId、responseData一同返还给oc的回调block。
继续往回找
function _handleMessageFromObjC(messageJSON) { _dispatchMessageFromObjC(messageJSON); }
-_handleMessageFromObjC:
这就很眼熟了。之前我们看到这里、然后说留到js这边分析。
现在想想他做了什么?
拿到OC发来的messageJSON。里面有responseId/handlerName以及responseData。然后通过responseId将js中对应的callback调起/执行指定已经注册函数。
然后、最后两个方法。
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName { //封装message@{callbackId/handlerName/data} [self _queueMessage:message]; } - (void)callHandler:(NSString *)handlerName data:(id)data responseCallback:(WVJBResponseCallback)responseCallback { [_base sendData:data responseCallback:responseCallback handlerName:handlerName]; }
callHandler发起调用、sendData发送数据。和js调用oc的时候简直一模一样。
这不是又完事了么...
嗯、本来以为调用的方式不一样。不过现在看来和js调用oc的方式基本相同。图也就不画了、直接去最上面看就行了。
最后、如果你也读了源码。肯定对responseId的存在表示疑问。
- responseId以及callbackId互斥。注册方发起指向调用方、后者表示调用方发起指向被调用方。
- 相应的。handlerName也只是在存在callbackId的时候才存在、并且执行handler。因为如果存在responseId、那个responseCallback就会直接被执行、完成回调、不会继续向下了。