JavaScriptBridge原理
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,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行等
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时使用
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");
}];
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);
}];
直接调用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];
});
}
}