WebViewJavaScriptBridge深刻剖析

前言

前一篇文章中,咱们大体的讲述了一下JavaScriptCore这个库在iOS开发中的应用。在文中最后的阶段,咱们提到了WebViewJavaScriptBridge这个库。提到这个库,可能有一些人就要说了,如今都什么时代了,谁还会用这个库啊?全是坑!不错,早在三年前,这个库有过一段辉煌的时光,在苹果除了WKWebView以后,渐渐的使用这个库的人愈来愈少,尽管这个库也是支持了WKWebView的。 可是一个事物的存在就有他的价值,就算使用也不是那么频繁了,尽管他有不少的坑。可是对于一个开发者来讲,咱们应该取其精华去其糟粕,现现在出的不少的交互的bridge依旧是有部分交互逻辑沿用了WebViewJavaScriptBridge的思想。 这里就不得不提味精大神的一片文章,这篇文章里面深刻浅出的谈了谈现现在Hybrid开发时经常使用的一些桥方法。有兴趣的能够去关注一下。废话很少说,那么咱们今天就从源码开始解析这个库的使用以及原理。javascript

简介

简单的来讲,在最开始的UIWebView时,原生跟JS之间的交互通常是两种方式:java

  • Native -> JS:这种方式很简单,只是是原生调用stringByEvaluatingJavaScriptFromString:方法,传入要执行的JS代码就能够实现;
  • JS -> Native:这种方式是在网页上面加载一串Custom URL Scheme的URL,而后经过原生去UIWebView的代理方法webView:shouldStartLoadWithRequest:navigationType:中拦截相应的URL作处理。

固然这个其实也就是WebViewJavaScriptBridge的理论核心。可是上面这种实现方法为何没有人使用呢?缘由就是,经过在代理方法里面拦截,咱们就必不可少的要写不少的if else的代码。在项目中的混合插件愈来愈多的时候,就致使了这个代理方法里面的逻辑愈来愈臃肿,愈来愈难以维护。 那么WebViewJavaScriptBridge的做用就是以更加优雅的方式,去实现Native与JS之间的互调。让Native能像调用OC的方法同样调用JS,同时JS也能像调用JS方法同样去调用OC。这就在OC和JS中间搭起了一座友谊的桥梁。git

使用

这里使用我就很少说了,直接pod 'WebViewJavascriptBridge'就能够引入到项目了。 附上源码地址:WebViewJavaScriptBridgegithub

目录结构

WebViewJavaScriptBridge目录结构

  • WebViewJavaScriptBridgeBase:bridge的核心类,用来初始化以及消息的处理;
  • WebViewJavaScriptBridge:判断WebView的类型,并经过不一样的类型进行分发。针对UIWebView和WebView作的一层封装,主要历来执行JS代码,以及实现UIWebView和WebView的代理方法,并经过拦截URL来通知WebViewJavaScriptBridgeBase作的相应操做;
  • WKWebViewJavaScriptBridge:主要是针对WKWebView作的一些封装,主要也是执行JS代码和实现WKWebView的代理方法的。同上面这个类相似;
  • WebViewJavaScriptBridge_JS:里面主要写了一些JS的方法,JS端与Native”互动“的JS端的方法基 本上都在这个里面;

主要流程

WebViewJavaScriptBridge参与交互的流程包括三个部分:初始化、JS调用Native、Native调用JS。接下来咱们就一一分析其中的过程。web

一、初始化

这里必需要说一下,WebViewJavaScriptBridge的这个设计很巧妙,他在JS端和Native端,都各自初始化了一个WebViewJavaScriptBridge对象,就像是两边各自安排了一个”通信兵“,让这两个对象去完成消息的收发工做。同时两边还各自维护一个管理相应事件的messageHandlers容器、一个管理回调的callbackId容器。因此这里的初始化,咱们得分为两个部分的初始化,一个部分是Native端的初始化,一个是JS端的初始化。这里咱们都以UIWebView为例子讲解,WKWebView其实也是相相似的原理,能够类比一下。json

(1)、Native端的初始化
  • 首先初始化WebViewJavaScriptBridge而且设置好代理
_bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
[_bridge setWebViewDelegate:self];
复制代码
- (void) _setupInstance:(WKWebView*)webView {
    _webView = webView;
    _webView.navigationDelegate = self;
    _base = [[WebViewJavascriptBridgeBase alloc] init];
    _base.delegate = self;
}
复制代码

而后其内部初始化了WebViewJavaScriptBridgeBase类和相关的属性bash

- (id)init {
    if (self = [super init]) {
        self.messageHandlers = [NSMutableDictionary dictionary];
        self.startupMessageQueue = [NSMutableArray array];
        self.responseCallbacks = [NSMutableDictionary dictionary];
        _uniqueId = 0;
    }
    return self;
}
复制代码
  • 注册handler,这个handler是提供给JS调用的
[_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"testObjcCallback called: %@", data);
        responseCallback(@"Response from testObjcCallback");
    }];
复制代码

注册其实就是在messageHandlers这个NSMutableDictionary里面保存一下app

- (void)registerHandler:(NSString *)handlerName handler:(WVJBHandler)handler {
    _base.messageHandlers[handlerName] = [handler copy];
}
复制代码
(2)、web view端的初始化
  • 当咱们经过loadRequest加载URL以后,网页一加载就会执行网页JS中的bridge的初始化方法setupWebViewJavascriptBridge函数
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)
    }
复制代码

这里主要作了两件事情,一个是保存要执行的一直自定义初始化函数,好比注册JS中的handler,第二个就是经过添加一个iframe加载初始化连接https://__bridge_loaded__函数

  • Native端会拦截https://__bridge_loaded__这个URL

WebViewJavaScriptBridge类中的拦截方法

  • 在webview中执行本地WebViewJavaScriptBridge_JS中的代码,初始化window.WebViewJavaScriptBridge对象:首先在JS中建立一个WebViewJavaScriptBridge对象,设置成window一个属性,而后定义几个用于管理消息的全局变量,接着给WebViewJavaScriptBridge对象定义几个处理消息的方法和函数,执行Native端startupMessageQueue中保存的消息,也就是本地JS文件还未加载时就发送了的消息。
window.WebViewJavascriptBridge = {
		registerHandler: registerHandler,
		callHandler: callHandler,
		disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout,
		_fetchQueue: _fetchQueue,
		_handleMessageFromObjC: _handleMessageFromObjC
	};
复制代码

二、JS调用Native

  • JS中调用callHandler()方法,发消息给原生
bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
				log('JS got response', response)
			})
复制代码

而后咱们看看callHandler是怎么定义的学习

function callHandler(handlerName, data, responseCallback) {
		if (arguments.length == 2 && typeof data == 'function') {
			responseCallback = data;
			data = null;
		}
		_doSend({ handlerName:handlerName, data:data }, responseCallback);
	}
复制代码

那么这个_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;
	}
复制代码

这下咱们清楚了,原来咱们在传入handlerNamedata被包装成了一个message传入到_doSend函数,而后生成一个callbackId,也一道包装到message中去。这样三个数据都被打包成了一个message传到Native。 固然为何要传入一个callbackId进去呢?这是由于用于处理原生回调的responseCallback是一个函数,是不能直接传给原生的,因此这里就把这个responseCallback存到了一个全局的responseCallbacks对象的属性里面去,属性名就是responseCallback对应的id。这个地方就是为了后面Native回调JS时,根据id找到对应的responseCallback

  • 在上图中的最后一步指的是JS会在iframe中加载发送消息的URL,此时原生就能够在相应的代理中拦截到这个URL,而后就知道JS端给我传递消息了,而后Native端会去调用JS,把sendMessageQueue中的message取出来,转成JSON string的格式。接着原生把JSON string解析成字典,取出相应的datacallbackIdhandlerName。最后根据handlerName去先前的messageHanlers里面取出相对应的block(handler),而后调用这个blockdata做为第一个参数,第二个参数是根据callbackId建立的responseCallback(block),而后原生就能够在block(handler)中处理接收到的data以及回调JS了。

  • 若是说须要原生给JS回调的话,当这个responseCallback被回调的时候,会执行下面的代码

- (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];
}
复制代码

这里就是直接建立了一个message(NSMutableDictionary)对象,把datacallbackIdhandlerName封装以后转换成为JSON string,最后调用WebViewJavascriptBridge._handleMessageFromObjC('%@')这个方法,把message传给JS。

- (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];
        });
    }
}
复制代码

在JS接收到了这个message以后,会根据里面的callbackId找到以前的responseCallback,把data做为参数,回调这个responseCallback

二、Native调用JS

其Native调用JS和上面JS调用Native是有不少的类似之处的。固然,其实也是能够直接经过web view执行JS脚本去实现的。可是WebViewJavaScriptBridge使用了一套更加规范的调用方式。接下来来介绍一下这种方式。

  • Native调用callHandler()方法,把消息发送给JS
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"foo":@"before ready" }];
复制代码

这个方法跟JS里面的这个方法名是同样的,固然实际的做用其实也是类似的。 在这里都是将handlerNamedataresponseCallback对应的id包装成一个message。而后把这个message对象转成JSON string。最后在调用WebViewJavascriptBridge._handleMessageFromObjC(messageJSON)方法把数据给到JS。这里至于为何也是传id,其实原理跟上面是同样的,block也是不能直接传给JS的,因此这里把responseCallback的这个block存到了全局的responseCallbacks字典里面去了,key就是responseCallback对应的id。JS回调Native的时候,就会来这个字典里面去取对应的block。其实思想都是差很少的。

  • JS端拿到了这个message以后,会将它解析成为JS对象,而后去使用datacallbackIdhandlerName。而后根据handlerNamemessageHandlers里面去对应的handler函数,而后去执行这个函数。第一个参数是传过来的data,第二个参数就是根据callbackId建立的responseCallback的function。这里就能够在handler里面处理接收到的回调了。
  • 这里与前面JS调Native时Native回调JS的处理不太同样,由于JS调Native是不能直接调的。可是怎么去通知Native呢?其实他这里就是直接走了JS调用Native的流程,就是上面提到的这个流程。不过仍是有不一样的:
    • 一是message里面的东西不同了;
    • 二是Native对message的处理:
      • 跟上面JS调用Native不同的就是message里面如今不须要你传一个callbackId了,由于这里原本就是JS回调给Native的,再传这个,两边就一直在回调来回调去了。可是呢,多了一个responseId,这是由于Native执行JS回调的时候,会根据这个responseIdresponseCallbacks中去取对应的block

        ```
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        ```
        复制代码
        • Native在收到JS回调以后,会根据responseId找到以前保存的responseCallback的block,而后把message中的responseData(其实就是data)做为参数回调给这个responseCallback。与JS调用Native不一样的其实就是这里的responseCallback只有一个data参数了,是没有用于再次回调JS的block了。

总结

至此,WebViewJavaScriptBridge的总体核心流程就基本上讲完了。这样看看,其实其中的原理还算是简单,可是很巧妙。两边都维护了一个WebViewJavaScriptBridge的对象,消息都封装成为一个message,而后全部的callback,都巧妙的转换成了id。经过直接传递id,而后根据id分别去对应的地方去寻找到对应的callback。这种方式,其实也是值得咱们去学习和使用的。 接下来我会继续的去研究如今比较火爆的JSCore的交互方式,对于Hybrid开发有想法的朋友,欢迎留言跟我交流。

相关文章
相关标签/搜索