一、初识JsBridge Hybrid APP基础篇(四)->JSBridge的原理:https://www.cnblogs.com/dailc/p/5931324.html

初次接触JsBridge,看了一些相关博客文章,确实有前辈写的很不错,看了后整个过程都清晰了,应用的时候一些疑惑也能找到答案,很开心,本篇文章参考大神博客,

想要好好学习一下的人可以直接移步上面的链接一定会有所收获的,以下只是根据自己的理解进行了一些整理总结,毕竟每个人都有自己的学习思路和习惯,看自己的笔记还是比较利于日后有需要的时候查阅和巩固的。

一、什么是JsBridge?

当开发hybrid App的时候,这时出于各种各样的因素考虑app的一部分由原生(ios和Android)进行开发,一部分由前端开发最后将h5页面嵌入到app中。这种情况就有原生和h5之间通信的需要了,比如一个页面是h5开发的,由于登录部分的逻辑原生和h5各自有不同的处理,那么,当在app中的h5页面中有一个按钮,点击后需要先进行登陆状态的判断,如果是未登录,先进行登陆,若登陆了,在执行他的操作,那么未登录时,去登录这个动作就要执行原生的登录逻辑,在h5页面想要执行原生的登录方法,这就是h5和原生之间需要进行一个通信。

JSBridge是Native(原生)代码与JS代码的通信桥梁。

Hybrid架构的核心就是JSBridge交互。作为前端,我们感兴趣的是Native(ios和Android)和h5(前端)到底是怎样进行交互的。

二、h5和原生的两种交互方式:

url scheme

  • 适用于所有的系统设备(低版本Android和低版本iOS都适用)
  • 通过url拦截实现的,在大量数据传输,以及效率上都有影响

JavaScriptCore(在Android中是addJavascriptInterface

  • 在低版本中会有这样或那样的问题,如JavaScriptCore不支持iOS7以下,addJavascriptInterface在4.2以前有风险漏洞(时至今日,这些低版本造成的影响已经慢慢不再)

三、url scheme

3.1、什么是url scheme?

(1)是一种类似url的链接,为了app之间相互调用而设计的(如微信的scheme=> weixin://,这种scheme必须原生app注册后才会生效,)。

可以用系统的OpenURI打开一个类似于url的链接(可拼入参数),然后系统会进行判断,如果是系统的url scheme,则打开系统应用,否则找看是否有app注册这种scheme,打开对应app。

(2)JsBridge中如何利用url scheme来实现前端与原生的交互呢?

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

3.2、JsBridge怎样应用url scheme来实现原生和h5的交互的呢?

在项目中使用JsBridge去做和app的联调(交互)时,我常会疑惑这几个问题JsBridge内部是怎么实现的,比如h5调用原生的方法JsBridge是怎样实现的?h5调用原生方法时需要传参以及回调方法的执行是怎么做的呢?原生是怎样得知方法被调用的?原生又是如何调用h5方法的?

以下是JsBridge实现的几个关键步骤:

(1)使JsBridge成为h5页面的全局对象window的属性

var JSBridge = window.JSBridge || (window.JSBridge = {});

(2)这个全局的JSBridge对象有以下几个方法:

registerHandler( String,Function )   

H5调用的方法 ,用来注册本地JS方法,注册后Native(原生)可通过JSBridge调用。调用后会将方法注册到本地变量messageHandlers 中

参数:方法名称,方法

callHandler( String,JSON,Function )

H5调用的方法    用来调用原生开放的api,调用后实际上还是本地通过url scheme触发。调用时会将回调id存放到本地变量responseCallbacks

参数:方法名称,参数,回调方法

_handleMessageFromNative( JSON )

Native调用的方法   这个方法有两种用法一是原生调用H5页面注册的方法,二是原生通知H5页面执行回调方法

参数:JSON格式,有两种形式,如果是调用h5页面注册的方法参数为:方法名,参数,回调函数id;如果是通知h5页面执行回调参数为:回调函数id,传递的参数;

以下两个方法并不是JsBridge的方法,而是h5本地局部变量:

responseCallbacks:用于存放回调函数的一个集合,原生执行了h5调用的对应api完成后会根据回调函数id来调用h5的回调函数;

messageHandlers:用于存放h5本地注册的方法的一个集合,只有本地注册了的方法才能被原生调用

(3)现在已经知道了JsBridge对象内部有哪些方法了,来看下h5在调用原生的方法的时候,内部是怎样一个过程

h5运用JsBridge的callHandler方法来调用原生的方法,那么具体来看下callHandler是怎么实现的:

callHandler( String,JSON,Function )

  1. 先判断是否有回调函数,如果有,生成回调函数id,并将回调函数id和回调函数放到responseCallbacks回调函数集合中;
  2. 通过特定的参数转换方法,将传入的数据,方法名一起,拼接成一个url scheme
//url scheme的格式如
//基本有用信息就是后面的callbackId,handlerName与data
//原生捕获到这个scheme后会进行分析
var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data

  3、拼接好url scheme后使用内部早已创建好的一个隐藏的frame来触发url 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,这样就可以避免这个问题。---引自参考来源

 (4)原生如何得知api被调用

在上一步中,我们已经成功在H5页面中触发scheme,那么Native如何捕获scheme被触发呢?根据系统的不同ios和安卓有不同的处理。

Android捕获url scheme

WebViewClient里通过shouldoverrideurlloading可以捕获到url scheme的触发 

public boolean shouldOverrideUrlLoading(WebView view, String url){
	//读取到url后自行进行分析处理
	
	//如果返回false,则WebView处理链接url,如果返回true,代表WebView根据程序来执行url
	return true;
}	

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后自行进行处理

  (5)原生捕获到scheme url后,分析url-参数和回调的格式

Native接收到Url后,可以按照这种格式将回调参数id、api名、参数提取出来,然后按如下步骤进行:

    • 根据api名,在本地找寻对应的api方法,并且记录该方法执行完后的回调函数id
    • 根据提取出来的参数,根据定义好的参数进行转化

      如果是JSON格式需要手动转换,如果是String格式直接可以使用

    • 原生本地执行对应的api功能方法
    • 功能执行完毕后,找到这次api调用对应的回调函数id,然后连同需要传递的参数信息,组装成一个JSON格式的参数

回调的JSON格式为:{responseId:回调id,responseData:回调数据}

    • responseId String型 H5页面中对应需要执行的回调函数的id,在H5中生成url scheme时就已经产生
    • responseData JSON型 Native需要传递给H5的回调数据,是一个JSON格式: {code:(整型,调用是否成功,1成功,0失败),result:具体需要传递的结果信息,可以为任意类型,msg:一些其它信息,如调用错误时的错误信息}
  • 通过JSBridge通知H5页面回调

(6)原生调用js

这里有两种情况:一种是原生调用js本地注册的方法,一种是原生生同时js执行回调方法

//将回调信息传给H5
JSBridge._handleMessageFromNative(messageJSON);	
参数messageJSON根据情况的不同有两种不同的形式:

Native通知H5页面进行回调

回调的JSON格式为:{responseId:回调id,responseData:回调数据}

Native主动调用H5方法

Native主动调用H5方法时,数据格式是:{handlerName:api名,data:数据,callbackId:回调id}

  • handlerName String型 需要调用的,h5中开放的api的名称
  • data JSON型 需要传递的数据,固定为JSON格式(因为我们固定H5中注册的方法接收的第一个参数必须是JSON,第二个是回调函数)
  • callbackId String型 原生生成的回调函数id,h5执行完毕后通过url scheme通知原生api成功执行,并传递参数

注意,这一步中,如果Native调用的api是h5没有注册的,h5页面上会有对应的错误提示。

另外,H5调用Native时,Native处理完毕后一定要及时通知H5进行回调,要不然这个回调函数不会自动销毁,多了后会引发内存泄漏。

(7)h5中注册供原生调用的api

//注册一个测试函数
JSBridge.registerHandler('testH5Func',function(data,callback){
	alert('测试函数接收到数据:'+JSON.stringify(data));
	callback&&callback('测试回传数据...');
});

  registerHandler方法的两个参数:datacallback

data:原生传过来的数据

callback:是内部封装过一次的,执行callback后会触发url scheme,通知原生获取回调信息

如果真的有读者看到这里,了解了JsBridge整个工作过程,就会对工作中的用法多了很多理解,不会在自己调用原生的方法或者注册供原生调用的方法时感到很迷茫觉得自己像个机器人,一步步机械的按照别人的步骤去操作,知其然不知其所以然,这种感觉常常是让人极其痛苦的。

下面来扩展除了url scheme外的另一种原生和h5交互的方式:

前面提到的JSBridge都是基于url scheme的,但其实如果不考虑Android4.2以下,iOS7以下,其实也可以用另一套方案的,如下

  • Native调用JS的方法不变
  • JS调用Native是不再通过触发url scheme,而是采用自带的交互,比如

    Android中,原生通过 addJavascriptInterface开放一个统一的api给JS调用,然后将触发url scheme步骤变为调用这个api,其余步骤不变(相当于以前是url接收参数,现在变为api函数接收参数)

    iOS中,原生通过JavaScriptCore里面的方法来注册一个统一api,其余和Android中一样(这里就不需要主动获取参数了,因为参数可以直接由这个函数统一接收)

下面提供一份基础版本的JSBridge代码供大家参考(这里只介绍JS的实现):

(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);
			});
		}
	};
})();