JavaScriptBridge原理与实现

JavaScriptBridge原理

JavaScriptBridge原理与实现_第1张图片
img_hybrid_base_jsbridgePrinciple.png

URL Scheme是什么

由于苹果的app都是在沙盒中,相互是不能访问数据的。但是苹果还是给出了一个可以在app之间跳转的方法:URL Scheme。简单的说,URL Scheme就是一个可以让app相互之间可以跳转的协议。每个app的URL Scheme都是不一样的,如果存在一样的URL Scheme,那么系统就会响应先安装那个app的URL Scheme,因为后安装的app的URL Scheme被覆盖掉了,是不能被调用的。需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为(weixin://)

URL Scheme有什么作用

那么app之间的跳转有什么作用呢?我们所使用的每一个app就相当于一个功能,app的跳转可以使得每个app就像一个功能组件一样,帮助我们完成需要做的事情,比如三方支付,搜索,导航,分享等等。

URL Scheme怎么使用

要跳转到别人的app,就要知道别人的app的跳转协议是什么,需要传入什么参数,我们常见的跳转协议有下面这些:

1.打开Mail
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"mailto:// [email protected]"]]
2.打开电话
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"tel://18688886666"]];
3.打开SMS
[[UIApplication sharedApplication] openURL:[NSURL URLWithString:@"sms:18688886666"]];

而本文JSBridge中的url scheme则是仿照上述的形式的一种方式

具体为,app不会注册对应的scheme,而是由前端页面通过某种方式触发scheme(如用iframe.src),然后Native用某种方法捕获对应的url触发事件,然后拿到当前的触发url,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行等

JavaScriptBridge原理与实现_第2张图片
WX20170413-142611.png
WX20170413-142731.png
WX20170413-152120.png
JavaScriptBridge原理与实现_第3张图片
WX20170413-142931.png

iOS捕获url scheme

iOS中,UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们可以在webview中捕获url scheme的触发(原理是利用 shouldStartLoadWithRequest)

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
NSURL *url = [request URL];

NSString *requestString = [[request URL] absoluteString];
//获取利润url scheme后自行进行处理

之后Native捕获到了JS调用的url scheme,接下来就该到下一步分析url了

JS和Native的交互

JS和Native的交互主要通过发送WVJBMessage消息来完成,它是一个字典,有五个字段:

data,交互数据
callbackId,是一个字符串,结构为objc_cb_(_uniqueid),_uniqueId是一个全局自增的整型变量
handlerName,回调名称
responseId,js调用native时使用,对应callbackId,标示是否是针对请求的回复
responseData,js调用native时使用
JavaScriptBridge原理与实现_第4张图片
WX20170413-140526.png
WX20170413-155724.png

JS如何调用Native

在执行callHandler时,内部经历了以下步骤:

(1)判断是否有回调函数,如果有,生成一个回调函数id,并将id和对应回调添加进入回调函数集合responseCallbacks中
(2)通过特定的参数转换方法,将传入的数据,方法名一起,拼接成一个url scheme

//url scheme的格式如
//基本有用信息就是后面的callbackId,handlerName与data
//原生捕获到这个scheme后会进行分析
var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data

(3)使用内部早就创建好的一个隐藏iframe来触发scheme

//创建隐藏iframe过程
var messagingIframe = document.createElement('iframe');
messagingIframe.style.display = 'none';
document.documentElement.appendChild(messagingIframe);

//触发scheme
messagingIframe.src = uri;

注意,正常来说是可以通过window.location.href达到发起网络请求的效果的,但是有一个很严重的问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。所以JS端发起网络请求的时候,需要使用iframe,这样就可以避免这个问题。

举个例子来说就是在网页中有一个登录按钮,点击登录按钮后,具体的登录功能是由OC端实现的,即登录功能实现需要我们在工程里有一个类似loginMethod的函数去具体操作。

工作流程

站在实际开发的角度来解释,就是假如现在有一个网页,在网页中有个登录按钮需要通过JS调OC的方式实现。那么我们首先需要跟负责网页编码的人员(一般是后台)商定出一个方法名称,也就是给这个登录按钮点击事件取个名字,例如叫loginCallBack。然后我们需要在代码里注册这个事件并负责它的具体实现。当用户点击这个登录按钮的时候,后台就会通知给这个事件的注册者去执行,有点像block的执行顺序。
代码实现

假如我们现在商定了一个事件名称为loginFunc,我们来看一下代码实现。

  /***
  /@param registerHandler 要注册的事件名称(这里我们为loginFunc)
  /@param handel 回调block函数 当后台触发这个事件的时候会执行block里面的代码
 ***/
 [_bridge registerHandler:@"loginFunc" handler:^(id data, WVJBResponseCallback responseCallback) {
 // data 后台传过来的参数,例如用户名、密码等
 NSLog(@"testObjcCallback called: %@", data);

 //具体的登录事件的实现,这里的login代表实现登录功能的一个OC函数。
            [self login];   

 // responseCallback 给后台的回复    

    responseCallback(@"Response from testObjcCallback");
  }];
WX20170413-161018.png
WX20170413-160955.png

JS调用Native的技巧是:定义了一个专用scheme和host,在webView的

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType

代理中判断是否是这个scheme来判断请求是否来自jsBridge,通过host判断是否有新消息,如果有,则主动调用协议定义的js方法来获取消息。

- (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 ([[url scheme] isEqualToString:kCustomProtocolScheme]) {
    if ([[url host] isEqualToString:kQueueHasMessage]) {
        [self _flushMessageQueue];//确认js想调用native
    } else {
        NSLog(@"WebViewJavascriptBridge: WARNING: Received unknown WebViewJavascriptBridge command %@://%@", kCustomProtocolScheme, [url path]);
    }
    return NO;//来自jsbridge的请求不会加载
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
    return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
    return YES;
}
}

_flushMessage主要分为下面几个步骤:

1.主动调用WebViewJavascriptBridge._fetchQueue()获取消息队列;

2.循环遍历消息队列

3.解析message,针对不同情况进行不同的处理

- (void)_flushMessageQueue {
//调用jsBridge协议的方法来获取js端想发送的信息
NSString *messageQueueString = [_webView stringByEvaluatingJavaScriptFromString:@"WebViewJavascriptBridge._fetchQueue();"];

id messages = [self _deserializeMessageJSON:messageQueueString];
if (![messages isKindOfClass:[NSArray class]]) {
    NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [messages class], messages);
    return;
}
for (WVJBMessage* message in messages) {
    if (![message isKindOfClass:[WVJBMessage class]]) {
        NSLog(@"WebViewJavascriptBridge: WARNING: Invalid %@ received: %@", [message class], message);
        continue;
    }
    [self _log:@"RCVD" json:message];
    //取出responseId
    NSString* responseId = message[@"responseId"];
    if (responseId) {
        /*
          *这个分支说明这个消息是针对native调用js的响应
         *取出这个responseId的回调,并且执行完毕后从字典中移除
        */
        WVJBResponseCallback responseCallback = _responseCallbacks[responseId];
        responseCallback(message[@"responseData"]);
        [_responseCallbacks removeObjectForKey:responseId];
    } else {
        //这个分支说明是js主动想调用native
        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];//执行完毕后,发送消息给js,通知调用已经完毕
            };
        } else {
            responseCallback = ^(id ignoreResponseData) {
                // Do nothing
            };
        }//设置调用native后的处理回调

        WVJBHandler handler;
        if (message[@"handlerName"]) {
            handler = _messageHandlers[message[@"handlerName"]];
        } else {
            handler = _messageHandler;
        }

        if (!handler) {
            [NSException raise:@"WVJBNoHandlerException" format:@"No handler for message from JS: %@", message];
        }
        //执行调用
        handler(message[@"data"], responseCallback);
    }
}

}

Native如何调用JS

举个例子,我们的原生APP上有个输入框,我们输入完成后,让它显示在网页上面的用户名处。这样,我们就是OC要实现的一个事件让网页去真正实现了,也就是OC调用JS。

和JS调用OC的流程大致一样,还是需要和网页编写人员商定出一个事件名,然后在网页里面先把注册这样一个事件并把实现体写好,等到我们OC去触发这个事件(比如点击按钮)就会去网页里面找到这个事件的实现体并执行。

//不需要传参数,不需要后台返回执行结果
  [_bridge callHandler:@"registerFunc"];

//需要传参数,不需要从后台返回执行结果
  /***
   @param callHandler 商定的事件名称,用来调用网页里面相应的事件实现
   @param data id类型,相当于我们函数中的参数,向网页传递函数执行需要的参数
 ***/

 [_bridge callHandler:@"registerFunc" data:@"name"];

//需要传参数,需要从后台返回执行结果

 [_bridge callHandler:@"registerFunc" data:@"name" responseCallback:^(id responseData) {

    NSLog(@"后台执行完成后返回的数据");

  }];

我们可以单纯地向JS发送数据,比如我们可以在网页加载完成后向网页发送一条加载完成的消息,或者传一个标题。

  //不需要后台返回执行结果或数据
  [_bridge send:@"红色"];

  //需要后台返回执行结果或数据
    [_bridge send:@"红色" responseCallback:^(id responseData) {

    NSLog(@"后台执行完成后返回的数据 %@", responseData);

   }];
JavaScriptBridge原理与实现_第5张图片
WX20170413-140500.png

直接调用stringByEvaluatingJavaScriptFromString即可。主要分为两步步:

1.调用提供的send接口
2.构建一个message,如果队列存在塞入队列,如果不存在直接dispatch
下面是主要的源码:

-(void)_sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];

if (data) {
    message[@"data"] = data;
}

if (responseCallback) {
     //将回调注册到_responseCallbacks中
    NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
    _responseCallbacks[callbackId] = [responseCallback copy];
    message[@"callbackId"] = callbackId;
}

if (handlerName) {
    message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];

}

- (void)_queueMessage:(WVJBMessage*)message {
//如果queue存在,则加入到queue中,否则直接调用dispatchMessage
if (_startupMessageQueue) {
    [_startupMessageQueue addObject:message];
} else {
    [self _dispatchMessage:message];
}
}
//将Message字典序列化成JSON,主线程调用JS方法。
- (void)_dispatchMessage:(WVJBMessage*)message {
NSString *messageJSON = [self _serializeMessage:message];
[self _log:@"SEND" json:messageJSON];
NSLog(@"%@ %@", message ,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"];
NSLog(@"%@ %@", message ,messageJSON);
NSString* javascriptCommand = [NSString stringWithFormat:@"WebViewJavascriptBridge._handleMessageFromObjC('%@');", messageJSON];
if ([[NSThread currentThread] isMainThread]) {
    [_webView stringByEvaluatingJavaScriptFromString:javascriptCommand];
} else {
    __strong WVJB_WEBVIEW_TYPE* strongWebView = _webView;
    dispatch_sync(dispatch_get_main_queue(), ^{
        [strongWebView stringByEvaluatingJavaScriptFromString:javascriptCommand];
    });
}
}

你可能感兴趣的:(JavaScriptBridge原理与实现)