JSBridge實現原理html
前人栽樹,後臺乘涼,本文參考瞭如下來源前端
閱讀本文前,建議先閱讀如下文章android
上文中簡單的介紹了JSBridge,以及爲何要用JSBridge,本文詳細介紹它的實現原理git
JSBridge是Native代碼與JS代碼的通訊橋樑。目前的一種統一方案是:H5觸發url scheme->Native捕獲url scheme->原生分析,執行->原生調用h5。以下圖github
上圖中有提到url scheme這個概念,那這究竟是什麼呢?web
具體爲,能夠用系統的OpenURI打開一個相似於url的連接(可拼入參數),而後系統會進行判斷,若是是系統的url scheme,則打開系統應用,不然找看是否有app註冊這種scheme,打開對應appjson
須要注意的是,這種scheme必須原生app註冊後纔會生效,如微信的scheme爲(weixin://)api
具體爲,app不會註冊對應的scheme,而是由前端頁面經過某種方式觸發scheme(如用iframe.src),而後Native用某種方法捕獲對應的url觸發事件,而後拿到當前的觸發url,根據定義好的協議,分析當前觸發了那種方法,而後根據定義來執行等數組
基於上述的基本原理,如今開始設計一種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); }); } }; })();
因爲內容較多,已經單獨提取成一個模塊,參考 Hybrid APP基礎篇(五)->JSBridge實現示例