JavaScriptBridge原理与实现

JavaScriptBridge原理

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:// info@icloud.com"]]
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,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行等

WX20170413-142611.png
WX20170413-142731.png
WX20170413-152120.png
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时使用
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);

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

推荐阅读更多精彩内容

  • Android 自定义View的各种姿势1 Activity的显示之ViewRootImpl详解 Activity...
    passiontim阅读 171,907评论 25 707
  • 发现 关注 消息 iOS 第三方库、插件、知名博客总结 作者大灰狼的小绵羊哥哥关注 2017.06.26 09:4...
    肇东周阅读 12,068评论 4 62
  • NSTimer的生命周期问题 那么问题来了:如果我就是想让这个 NSTimer 一直输出,直到 DemoViewC...
    齐滇大圣阅读 1,179评论 0 6
  • 现在有很多有趣的文章,都在向你生动地讲述,如何使你成为一名最优秀的人:最高效的人,最具创造力的人,最佳倾听者,最睿...
    Jodoo阅读 37,990评论 11 133