前人栽树,后台乘凉,本文参考了如下来源html
阅读本文前,建议先阅读如下文章前端
上文中简单的介绍了JSBridge,以及为何要用JSBridge,本文详细介绍它的实现原理android
JSBridge是Native代码与JS代码的通讯桥梁。目前的一种统一方案是:H5触发url scheme->Native捕获url scheme->原生分析,执行->原生调用h5。以下图git
上图中有提到url scheme这个概念,那这究竟是什么呢?github
具体为,能够用系统的OpenURI打开一个相似于url的连接(可拼入参数),而后系统会进行判断,若是是系统的url scheme,则打开系统应用,不然找看是否有app注册这种scheme,打开对应appweb
须要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为(weixin://)json
具体为,app不会注册对应的scheme,而是由前端页面经过某种方式触发scheme(如用iframe.src),而后Native用某种方法捕获对应的url触发事件,而后拿到当前的触发url,根据定义好的协议,分析当前触发了那种方法,而后根据定义来执行等api
基于上述的基本原理,如今开始设计一种JSBridge的实现数组
要实现JSBridge,咱们能够进行关键步骤分析微信
以下图:
咱们规定,JS和Native之间的通讯必须经过一个H5全局对象JSbridge来实现,该对象有以下特色
var JSBridge = window.JSBridge || (window.JSBridge = {});
messageHandlers
中responseCallbacks
中在第一步中,咱们定义好了全局桥对象,能够咱们是经过它的callHandler方法来调用原生的,那么它内部经历了一个怎么样的过程呢?以下
在执行callHandler时,内部经历了如下步骤:
responseCallbacks
中//url scheme的格式如 //基本有用信息就是后面的callbackId,handlerName与data //原生捕获到这个scheme后会进行分析 var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data
//建立隐藏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,这样就能够避免这个问题。---引自参考来源
在上一步中,咱们已经成功在H5页面中触发scheme,那么Native如何捕获scheme被触发呢?
根据系统不一样,Android和iOS分别有本身的处理方式
在Android中(WebViewClient里),经过shouldoverrideurlloading
能够捕获到url scheme的触发
public boolean shouldOverrideUrlLoading(WebView view, String url){ //读取到url后自行进行分析处理 //若是返回false,则WebView处理连接url,若是返回true,表明WebView根据程序来执行url return true; }
另外,Android中也能够不经过iframe.src来触发scheme,android中能够经过window.prompt(uri, "");
来触发scheme,而后Native中经过重写WebViewClient的onJsPrompt
来获取uri
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了
在前面的步骤中,Native已经接收到了JS调用的方法,那么接下来,原生就应该按照定义好的数据格式来解析数据了
url scheme的格式 前面已经提到。Native接收到Url后,能够按照这种格式将回调参数id、api名、参数提取出来,而后按以下步骤进行
若是是JSON格式须要手动转换,若是是String格式直接可使用
回调的JSON格式为:{responseId:回调id,responseData:回调数据}
{code:(整型,调用是否成功,1成功,0失败),result:具体须要传递的结果信息,能够为任意类型,msg:一些其它信息,如调用错误时的错误信息}
参考 Native如何调用JS
到了这一步,就该Native经过JSBridge调用H5的JS方法或者通知H5进行回调了,具体以下
//将回调信息传给H5 JSBridge._handleMessageFromNative(messageJSON);
如上,其实是经过JSBridge的_handleMessageFromNative传递数据给H5,其中的messageJSON数据格式根据两种不一样的类型,有所区别,以下
数据格式为: Native通知H5回调的JSON格式
Native主动调用H5方法时,数据格式是:{handlerName:api名,data:数据,callbackId:回调id}
注意,这一步中,若是Native调用的api是h5没有注册的,h5页面上会有对应的错误提示。
另外,H5调用Native时,Native处理完毕后必定要及时通知H5进行回调,要否则这个回调函数不会自动销毁,多了后会引起内存泄漏。
前面有提到Native主动调用H5中注册的api方法,那么h5中怎么注册供原生调用的api方法呢?格式又是什么呢?以下
//注册一个测试函数 JSBridge.registerHandler('testH5Func',function(data,callback){ alert('测试函数接收到数据:'+JSON.stringify(data)); callback&&callback('测试回传数据...'); });
如上述代码为注册一个供原生调用的api
如上代码,注册的api参数是(data,callback)
其中第一个data即原生传过来的数据,第二个callback是内部封装过一次的,执行callback后会触发url scheme,通知原生获取回调信息
在前文中,已经完成了一套JSBridge方案,这里,在介绍下如何完善这套方案
github上有一个开源项目,它里面的JSBridge作法在iOS上进一步优化了,因此参考他的作法,这里进一步进行了完善。地址marcuswestin/WebViewJavascriptBridge
大体思路就是
完善之前: H5调用Native->将全部参数组装成为url scheme->原生捕获scheme,进行分析
完善之后: H5调用Native->将全部参数存入本地数组->触发一个固定的url scheme->原生捕获scheme->原生经过JSBridge主动获取参数->进行分析
这种完善后的流程和之前有所区别,以下
因为此次完善的核心是:Native主动调用JS函数,并获取返回值。而在Android4.4之前,Android是没有这个功能的,因此并不彻底适用于Android
因此通常会进行一个兼容处理,Android中采用之前的scheme传法,iOS使用完善后的方案(也便于4.4普及后后续的完善)
上述分析了JSBridge的实现流程,那么实际项目中,咱们就应该结合上述两种,针对Android和iOS的不一样状况,统一出一种完整的方案,以下
如上图,结合上述方案后有了一套统一JSBridge方案
前面提到的JSBridge都是基于url scheme的,但其实若是不考虑Android4.2如下,iOS7如下,其实也能够用另外一套方案的,以下
Android中,原生经过 addJavascriptInterface开放一个统一的api给JS调用,而后将触发url scheme步骤变为调用这个api,其他步骤不变(至关于之前是url接收参数,如今变为api函数接收参数)
iOS中,原生经过JavaScriptCore里面的方法来注册一个统一api,其他和Android中同样(这里就不须要主动获取参数了,由于参数能够直接由这个函数统一接收)
固然了,这只是一种可行的方案,多一种选择而已,具体实现流程请参考前面系列文章,本文再也不赘述
本文中包括两个示例,一个是基础版本的JSBridge实现,一个是完整版本的JSBridge实现(包括JS,Android,iOS实现等)
这里只介绍JS的实现,具体Android,iOS实现请参考完整版本,实现以下
(function() { (function() { var hasOwnProperty = Object.prototype.hasOwnProperty; var JSBridge = window.JSBridge || (window.JSBridge = {}); //jsbridge协议定义的名称 var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge'; //最外层的api名称 var API_Name = 'namespace_bridge'; //进行url scheme传值的iframe var messagingIframe = document.createElement('iframe'); messagingIframe.style.display = 'none'; messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name; document.documentElement.appendChild(messagingIframe); //定义的回调函数集合,在原生调用完对应的方法后,会执行对应的回调函数id var responseCallbacks = {}; //惟一id,用来确保每个回调函数的惟一性 var uniqueId = 1; //本地注册的方法集合,原生只能调用本地注册的方法,不然会提示错误 var messageHandlers = {}; //实际暴露给原生调用的对象 var Inner = { /** * @description 注册本地JS方法经过JSBridge给原生调用 * 咱们规定,原生必须经过JSBridge来调用H5的方法 * 注意,这里通常对本地函数有一些要求,要求第一个参数是data,第二个参数是callback * @param {String} handlerName 方法名 * @param {Function} handler 对应的方法 */ registerHandler: function(handlerName, handler) { messageHandlers[handlerName] = handler; }, /** * @description 调用原生开放的方法 * @param {String} handlerName 方法名 * @param {JSON} data 参数 * @param {Function} callback 回调函数 */ callHandler: function(handlerName, data, callback) { //若是没有 data if(arguments.length == 3 && typeof data == 'function') { callback = data; data = null; } _doSend({ handlerName: handlerName, data: data }, callback); }, /** * @description 原生调用H5页面注册的方法,或者调用回调方法 * @param {String} messageJSON 对应的方法的详情,须要手动转为json */ _handleMessageFromNative: function(messageJSON) { setTimeout(_doDispatchMessageFromNative); /** * @description 处理原生过来的方法 */ function _doDispatchMessageFromNative() { var message; try { if(typeof messageJSON === 'string'){ message = JSON.parse(messageJSON); }else{ message = messageJSON; } } catch(e) { //TODO handle the exception console.error("原生调用H5方法出错,传入参数错误"); return; } //回调函数 var responseCallback; if(message.responseId) { //这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseId responseCallback = responseCallbacks[message.responseId]; if(!responseCallback) { return; } //执行本地的回调函数 responseCallback(message.responseData); delete responseCallbacks[message.responseId]; } else { //不然,表明原生主动执行h5本地的函数 if(message.callbackId) { //先判断是否须要本地H5执行回调函数 //若是须要本地函数执行回调通知原生,那么在本地注册回调函数,而后再调用原生 //回调数据有h5函数执行完毕后传入 var callbackResponseId = message.callbackId; responseCallback = function(responseData) { //默认是调用EJS api上面的函数 //而后接下来原生知道scheme被调用后主动获取这个信息 //因此原生这时候应该会进行判断,判断对于函数是否成功执行,并接收数据 //这时候通信完毕(因为h5不会对回调添加回调,因此接下来没有通讯了) _doSend({ handlerName: message.handlerName, responseId: callbackResponseId, responseData: responseData }); }; } //从本地注册的函数中获取 var handler = messageHandlers[message.handlerName]; if(!handler) { //本地没有注册这个函数 } else { //执行本地函数,按照要求传入数据和回调 handler(message.data, responseCallback); } } } } }; /** * @description JS调用原生方法前,会先send到这里进行处理 * @param {JSON} message 调用的方法详情,包括方法名,参数 * @param {Function} responseCallback 调用完方法后的回调 */ function _doSend(message, responseCallback) { if(responseCallback) { //取到一个惟一的callbackid var callbackId = Util.getCallbackId(); //回调函数添加到集合中 responseCallbacks[callbackId] = responseCallback; //方法的详情添加回调函数的关键标识 message['callbackId'] = callbackId; } //获取 触发方法的url scheme var uri = Util.getUri(message); //采用iframe跳转scheme的方法 messagingIframe.src = uri; } var Util = { getCallbackId: function() { //若是没法解析端口,能够换为Math.floor(Math.random() * (1 << 30)); return 'cb_' + (uniqueId++) + '_' + new Date().getTime(); }, //获取url scheme //第二个参数是兼容android中的作法 //android中因为原生不能获取JS函数的返回值,因此得经过协议传输 getUri: function(message) { var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name; if(message) { //回调id做为端口存在 var callbackId, method, params; if(message.callbackId) { //第一种:h5主动调用原生 callbackId = message.callbackId; method = message.handlerName; params = message.data; } else if(message.responseId) { //第二种:原生调用h5后,h5回调 //这种状况下须要原生自行分析传过去的port是不是它定义的回调 callbackId = message.responseId; method = message.handlerName; params = message.responseData; } //参数转为字符串 params = this.getParam(params); //uri 补充 uri += ':' + callbackId + '/' + method + '?' + params; } return uri; }, getParam: function(obj) { if(obj && typeof obj === 'object') { return JSON.stringify(obj); } else { return obj || ''; } } }; for(var key in Inner) { if(!hasOwnProperty.call(JSBridge, key)) { JSBridge[key] = Inner[key]; } } })(); //注册一个测试函数 JSBridge.registerHandler('testH5Func', function(data, callback) { alert('测试函数接收到数据:' + JSON.stringify(data)); callback && callback('测试回传数据...'); }); /* ***************************API******************************************** * 开放给外界调用的api * */ window.jsapi = {}; /** ***app 模块 * 一些特殊操做 */ jsapi.app = { /** * @description 测试函数 */ testNativeFunc: function() { //调用一个测试函数 JSBridge.callHandler('testNativeFunc', {}, function(res) { callback && callback(res); }); } }; })();
文章来源:http://www.cnblogs.com/dailc/p/5931324.html