原文鏈接 https://www.cnblogs.com/dailc/p/5931324.html#hybrid_2_2
說明
JSBridge實作原理
目錄
- 前言
- 參考來源
- 前置技術要求
- 楔子
- 原理概述
- 簡介
- url scheme介紹
- 實作流程
- 實作思路
- 第一步:設計出一個Native與JS互動的全域橋物件
- 第二步:JS如何呼叫Native
- 第三步:Native如何得知api被呼叫
- 第四步:分析url-引數和回呼的格式
- 第五步:Native如何呼叫JS
- 第六步:H5中api方法的注冊以及格式
- 進一步完善JSBridge方案
- 思路
- 實作
- 注意
- 完整的JSBridge
- 完整呼叫流程圖
- 另外實作:不采用url scheme方式
- 實作示例
- 示例說明
- 實作原始碼
前言
參考來源
前人栽樹,后臺乘涼,本文參考了以下來源
- github-WebViewJavascriptBridge
- JSBridge-Web與Native互動之iOS篇
- Ios Android Hybrid app 與 Js Bridge
- Hybrid APP架構設計思路
- 【Android】如何寫一個JsBridge
- IOS之URL Scheme的使用
- marcuswestin/WebViewJavascriptBridge
前置技術要求
閱讀本文前,建議先閱讀以下文章
- Hybrid APP基礎篇(三)->Hybrid APP之Native和H5頁面互動原理
楔子
上文中簡單的介紹了JSBridge,以及為什么要用JSBridge,本文詳細介紹它的實作原理
原理概述
簡介
JSBridge是Native代碼與JS代碼的通信橋梁,目前的一種統一方案是:H5觸發url scheme->Native捕獲url scheme->原生分析,執行->原生呼叫h5,如下圖

url scheme介紹
上圖中有提到url scheme這個概念,那這到底是什么呢?
-
url scheme是一種類似于url的鏈接,是為了方便app直接互相呼叫設計的
具體為,可以用系統的OpenURI打開一個類似于url的鏈接(可拼入引數),然后系統會進行判斷,如果是系統的url scheme,則打開系統應用,否則找看是否有app注冊這種scheme,打開對應app
需要注意的是,這種scheme必須原生app注冊后才會生效,如微信的scheme為(weixin://)
-
而本文JSBridge中的url scheme則是仿照上述的形式的一種方式
具體為,app不會注冊對應的scheme,而是由前端頁面通過某種方式觸發scheme(如用iframe.src),然后Native用某種方法捕獲對應的url觸發事件,然后拿到當前的觸發url,根據定義好的協議,分析當前觸發了那種方法,然后根據定義來執行等
實作流程
基于上述的基本原理,現在開始設計一種JSBridge的實作
實作思路
要實作JSBridge,我們可以進行關鍵步驟分析
- 第一步:設計出一個Native與JS互動的全域橋物件
- 第二步:JS如何呼叫Native
- 第三步:Native如何得知api被呼叫
- 第四步:分析url-引數和回呼的格式
- 第五步:Native如何呼叫JS
- 第六步:H5中api方法的注冊以及格式
如下圖:

第一步:設計出一個Native與JS互動的全域橋物件
我們規定,JS和Native之間的通信必須通過一個H5全域物件JSbridge來實作,該物件有如下特點
-
該物件名為"JSBridge",是H5頁面中全域物件window的一個屬性
var JSBridge = window.JSBridge || (window.JSBridge = {}); -
該物件有如下方法
- registerHandler( String,Function )H5呼叫 注冊本地JS方法,注冊后Native可通過JSBridge呼叫,呼叫后會將方法注冊到本地變數
messageHandlers中 - callHandler( String,JSON,Function )H5呼叫 呼叫原生開放的api,呼叫后實際上還是本地通過url scheme觸發,呼叫時會將回呼id存放到本地變數
responseCallbacks中 - _handleMessageFromNative( JSON )Native呼叫 原生呼叫H5頁面注冊的方法,或者通知H5頁面執行回呼方法
- registerHandler( String,Function )H5呼叫 注冊本地JS方法,注冊后Native可通過JSBridge呼叫,呼叫后會將方法注冊到本地變數
-
如圖

第二步:JS如何呼叫Native
在第一步中,我們定義好了全域橋物件,可以我們是通過它的callHandler方法來呼叫原生的,那么它內部經歷了一個怎么樣的程序呢?如下
callHandler函式內部實作程序
在執行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,這樣就可以避免這個問題,---引自參考來源
第三步:Native如何得知api被呼叫
在上一步中,我們已經成功在H5頁面中觸發scheme,那么Native如何捕獲scheme被觸發呢?
根據系統不同,Android和iOS分別有自己的處理方式
Android捕獲url scheme
在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捕獲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了
第四步:分析url-引數和回呼的格式
在前面的步驟中,Native已經接收到了JS呼叫的方法,那么接下來,原生就應該按照定義好的資料格式來決議資料了
url scheme的格式 前面已經提到,Native接收到Url后,可以按照這種格式將回呼引數id、api名、引數提取出來,然后按如下步驟進行
-
(1)根據api名,在本地找尋對應的api方法,并且記錄該方法執行完后的回呼函式id
-
(2)根據提取出來的引數,根據定義好的引數進行轉化
如果是JSON格式需要手動轉換,如果是String格式直接可以使用
-
(3)原生本地執行對應的api功能方法
-
(4)功能執行完畢后,找到這次api呼叫對應的回呼函式id,然后連同需要傳遞的引數資訊,組裝成一個JSON格式的引數
回呼的JSON格式為:
{responseId:回呼id,responseData:回呼資料}- responseId String型 H5頁面中對應需要執行的回呼函式的id,在H5中生成url scheme時就已經產生
- responseData JSON型 Native需要傳遞給H5的回呼資料,是一個JSON格式:
{code:(整型,呼叫是否成功,1成功,0失敗),result:具體需要傳遞的結果資訊,可以為任意型別,msg:一些其它資訊,如呼叫錯誤時的錯誤資訊}
-
(5)通過JSBridge通知H5頁面回呼
參考 Native如何呼叫JS
第五步:Native如何呼叫JS
到了這一步,就該Native通過JSBridge呼叫H5的JS方法或者通知H5進行回呼了,具體如下
//將回呼資訊傳給H5
JSBridge._handleMessageFromNative(messageJSON);
如上,實際上是通過JSBridge的_handleMessageFromNative傳遞資料給H5,其中的messageJSON資料格式根據兩種不同的型別,有所區別,如下
Native通知H5頁面進行回呼
資料格式為: Native通知H5回呼的JSON格式
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進行回呼,要不然這個回呼函式不會自動銷毀,多了后會引發記憶體泄漏,
第六步:H5中api方法的注冊以及格式
前面有提到Native主動呼叫H5中注冊的api方法,那么h5中怎么注冊供原生呼叫的api方法呢?格式又是什么呢?如下
H5中注冊供原生呼叫的API
//注冊一個測驗函式
JSBridge.registerHandler('testH5Func',function(data,callback){
alert('測驗函式接收到資料:'+JSON.stringify(data));
callback&&callback('測驗回傳資料...');
});
如上述代碼為注冊一個供原生呼叫的api
H5中注冊的API格式注意
如上代碼,注冊的api引數是(data,callback)
其中第一個data即原生傳過來的資料,第二個callback是內部封裝過一次的,執行callback后會觸發url scheme,通知原生獲取回呼資訊
進一步完善JSBridge方案
在前文中,已經完成了一套JSBridge方案,這里,在介紹下如何完善這套方案
思路
github上有一個開源專案,它里面的JSBridge做法在iOS上進一步優化了,所以參考他的做法,這里進一步進行了完善,地址marcuswestin/WebViewJavascriptBridge
大致思路就是
-
h5呼叫Native的關鍵步驟進行拆分,由以前的直接傳遞url scheme變為傳遞一個統一的url scheme,然后Native主動獲取傳遞的引數
完善以前: H5呼叫Native->將所有引陣列裝成為url scheme->原生捕獲scheme,進行分析
完善以后: H5呼叫Native->將所有引數存入本地陣列->觸發一個固定的url scheme->原生捕獲scheme->原生通過JSBridge主動獲取引數->進行分析
實作
這種完善后的流程和以前有所區別,如下
JSBridge物件圖解

JSBridge實作完整流程

注意
由于這次完善的核心是:Native主動呼叫JS函式,并獲取回傳值,而在Android4.4以前,Android是沒有這個功能的,所以并不完全適用于Android
所以一般會進行一個兼容處理,Android中采用以前的scheme傳法,iOS使用完善后的方案(也便于4.4普及后后續的完善)
完整的JSBridge
上述分析了JSBridge的實作流程,那么實際專案中,我們就應該結合上述兩種,針對Android和iOS的不同情況,統一出一種完整的方案,如下
完整呼叫流程圖

如上圖,結合上述方案后有了一套統一JSBridge方案
另外實作:不采用url scheme方式
前面提到的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實作,一個是完整版本的JSBridge實作(包括JS,Android,iOS實作等)
實作原始碼
基礎版本的JSBridge
這里只介紹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);
});
}
};
})();
完整版本的JSBridge
轉載請註明出處,本文鏈接:https://www.uj5u.com/yidong/255251.html
標籤:其他
上一篇:android博客導航總結,以及個人常用android免費學習干貨(文章,視頻,矢量圖,字體等)資源分享?
下一篇:需求
