IOS、Android與H5通訊-JsBridge原理(總結)

不多在網絡上寫文章,這一系列文章就是藉助掘金這個平臺來記錄一些東西,到時候本身想看的時候容易找到;惋惜掘金不能設置訪問權限 :( 若是有錯誤,你們請自行百度正確答案,謝謝!!android

有摘錄,有原創:)ios


H5和原生app(ios,android)交互的載體基本都是基於Webview,能夠把Webview看做是一個性能打八折的移動瀏覽器。web


ios調用Javascript

簡單說下這幾種:WKWebView 、UIWebView、JavaScriptCorechrome

WKWebView:蘋果在ios8以後也引入了專門負責處理網頁視圖的框架WebKit,Webkit是啥,接觸過H五、chrome的確定都知道。chrome使用的也是基於webkit內核的Chromium引擎。WKWebView優勢不少,支持更多H5特性,刷新效率及內置手勢等,更增強大,性能也更優,不一一列舉,若是你們app不須要兼容7及如下版本,不須要攔截一些請求,直接解析本地一些文件,建議使用WKWebView。json

UIWebView:較老webview,第一代。其中stringByEvaluatingJavaScriptFromString方法提供了OC與js交互的能力。小程序

JavaScriptCore(ios7及之後版本)。JavaScriptCore框架是webkit重要組成部分,主要是對JS進行解析和提供執行環境,Javascript的虛擬機,有點相似v8引擎,我本身這麼理解:)正是它爲ios提供了執行JavaScript代碼的能力。ReactNative應該都是經過JavaScriptCore去解析的(本身猜想)。swift

微信小程序的邏輯層也是由JavaScriptCore做爲運行環境。微信小程序


Javascript 調用 ios(oc、swift)原理:

目前兼顧兼容性、比較成熟的方案仍是經過攔截URL的方式。api

UIWebView的特性,在UIWebView內發起的全部網絡請求,均可以在Native層被捕捉到。瀏覽器

利用這一特性,就能夠在UIWebView內發起一個自定義的網絡請求,通常格式:jsbridge://method?參數1=value1&參數2=value2

因而在UIWebView中,只要發現是jsbridge://開頭的url,就不進行內容的加載,而是執行相應的邏輯處理。

嵌入webview的h5中的js通常是經過動態建立隱藏iframe標籤,賦值上文提到的連接給src,iframe不會引發頁面調轉、刷新。

主要代碼:

var src= 'jsbridge://method?參數1=value1&參數2=value2';
var iframe = document.createElement('iframe');
iframe.style.display = 'none';
iframe.src = src;
document.body.appendChild(iframe);
//再刪除iframesetTimeout(function() {
    iframe.remove();
}, 50);
複製代碼


Android和Javascript互相調用

Android WebView也是基於WebKit引擎的一個組件,Android的Webview在低版本和高版本採用了不一樣的webkit版本內核,4.4後直接使用了Chrome。

這個組件功能很是強大,除了具備通常View的屬性和設置外,還能夠對url請求、頁面加載、渲染、頁面交互進行強大的處理。

Android調用JS代碼的方法主要有2種:
  1. WebView的loadUrl
  2. WebView的evaluateJavascript
JS調用Android代碼的方法主要有3種:
  1. WebView的addJavascriptInterface進行對象映射(低版本Android4如下好像有一些安全問題,本人沒有驗證)
  2. WebViewClient 的 shouldOverrideUrlLoading 方法回調攔截 url
  3. WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt方法回調攔截JS對話框alert()、confirm()、prompt() 消息

通常經常使用onJsPrompt、prompt進行回調攔截


囉嗦了這麼多,還沒說到主題,JsBridge。一句話,JSBridge是Native代碼與JS代碼的通訊橋樑。


設計一個jsbridge主要分幾大步驟:

第一步:設計出一個Native與JS交互的全局中間對象

第二步:JS如何調用Native

第三步:Native如何得知api被調用

第四步:分析url-參數和回調的格式

第五步:Native如何調用JS

第六步:H5中api方法的註冊以及格式


H5端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 = {};
		//當原生調用H5註冊的方法時,經過回調來調用(也就是變爲了異步執行,增強安全性)
		var dispatchMessagesWithTimeoutSafety = true;
		//本地運行中的方法隊列
		var sendMessageQueue = [];

		//實際暴露給原生調用的對象
		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);
			},
			/**
			 * iOS專用
			 * @description 當本地調用了callHandler以後,實際是調用了通用的scheme,通知原生
			 * 而後原生經過調用這個方法來獲知當前正在調用的方法隊列
			 */
			_fetchQueue: function() {
				var messageQueueString = JSON.stringify(sendMessageQueue);
				sendMessageQueue = [];
				return messageQueueString;
			},
			/**
			 * @description 原生調用H5頁面註冊的方法,或者調用回調方法
			 * @param {String} messageJSON 對應的方法的詳情,須要手動轉爲json
			 */
			_handleMessageFromNative: function(messageJSON) {
				setTimeout(_doDispatchMessageFromNative);
				/**
				 * @description 處理原生過來的方法
				 */
				function _doDispatchMessageFromNative() {
					var message;
					try {
						message = JSON.parse(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;
			}
			var uri;
			//android中,能夠經過onJsPrompt或者截取Url訪問都行
			var ua = navigator.userAgent;
			if(ua.match(/(iPhone\sOS)\s([\d_]+)/)||ua.match(/(iPad).*OS\s([\d_]+)/)) {
				//ios中,經過截取客戶端url訪問
				//由於ios能夠不暴露scheme,而是由原生手動獲取
				//正在調用的方法詳情添加進入消息隊列中,原生會主動獲取
				sendMessageQueue.push(message);
				uri = Util.getUri();
			}else{
				//android中兼容處理,將全部的參數一塊兒拼接到url中
				uri = Util.getUri(message);
			}
			//獲取 觸發方法的url scheme
			//採用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);
			});
		}
	};
})();	複製代碼
相關文章
相關標籤/搜索