概述
做過混合開發的人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎上包裝一層Native,然後通過Bridge技術的js調用本地的庫。
在講JSBridge技術之前,我們來看一下傳統的實現方式。
Android端
Native調JS
native調用js比較簡單,只要遵循:”javascript: 方法名(‘參數,需要轉爲字符串’)”的規則即可。
在4.4之前,調用的方式:
-
// mWebView = new WebView(this);
-
mWebView.loadUrl("javascript: 方法名('參數,需要轉爲字符串')");
-
//ui線程中運行
-
runOnUiThread(new Runnable() {
-
@Override
-
public void run() {
-
mWebView.loadUrl("javascript: 方法名('參數,需要轉爲字符串')");
-
Toast.makeText(Activity名.this, "調用方法...", Toast.LENGTH_SHORT).show();
-
}
-
});
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
4.4以後(包括4.4),使用以下方式:
-
mWebView.evaluateJavascript("javascript: 方法名('參數,需要轉爲字符串')", new ValueCallback() {
-
@Override
-
public void onReceiveValue(String value) {
-
//這裏的value即爲對應JS方法的返回值
-
}
-
});
- 1
- 2
- 3
- 4
- 5
- 6
- 1
- 2
- 3
- 4
- 5
- 6
說明:
- 4.4之前Native通過loadUrl來調用JS方法,只能讓某個JS方法執行,但是無法獲取該方法的返回值
- 4.4之後,通過evaluateJavascript異步調用JS方法,並且能在onReceiveValue中拿到返回值
- 不適合傳輸大量數據(大量數據建議用接口方式獲取)
- mWebView.loadUrl(“javascript: 方法名(‘參數,需要轉爲字符串’)”);函數需在UI線程運行,因爲mWebView爲UI控件
JS調Native
Js調用Native需要對WebView設置@JavascriptInterface註解,這裏有個漏洞,後面會給大家說明。要想js能夠Native,需要對WebView設置以下屬性。
-
WebSettings webSettings = mWebView.getSettings();
-
//Android容器允許JS腳本
-
webSettings.setJavaScriptEnabled(true);
-
//Android容器設置僑連對象
-
mWebView.addJavascriptInterface(getJSBridge(), "JSBridge");
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
這裏我們看到了getJSBridge(),Native中通過addJavascriptInterface添加暴露出來的JS橋對象,然後再該對象內部聲明對應的API方法。
-
private Object getJSBridge(){
-
Object insertObj = new Object(){
-
@JavascriptInterface
-
public String foo(){
-
return "foo";
-
}
-
@JavascriptInterface
-
public String foo2(final String param){
-
return "foo2:" + param;
-
}
-
};
-
return insertObj;
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
那麼Html怎麼調用Native的方法呢?
-
//調用方法一
-
window.JSBridge.foo(); //返回:'foo'
-
//調用方法二
-
window.JSBridge.foo2('test');//返回:'foo2:test'
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
說明:
- 在Android4.2以上(api17後),暴露的api要加上註解@JavascriptInterface,否則會找不到方法。
- 在api17以前,addJavascriptInterface有風險,hacker可以通過反編譯獲取Native註冊的Js對象,然後在頁面通過反射Java的內置 靜態類,獲取一些敏感的信息和破壞
- JS調用Native暴露的api,並且能得到相應返回值
注:說到WebView中接口隱患的問題,這裏大家可以參考WebViw漏洞利用,不過Android發展到現在,這個漏洞基本沒有了。
iOS端
Native調JS
Native調用js的方法比較簡單,Native通過stringByEvaluatingJavaScriptFromString調用Html綁定在window上的函數。不過應注意Oc和Swift的寫法。
-
//Swift
-
webview.stringByEvaluatingJavaScriptFromString("方法名(參數)")
-
//OC
-
[webView stringByEvaluatingJavaScriptFromString:@"方法名(參數);"];
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
說明:
- Native調用JS方法時,能拿到JS方法的返回值
- 不適合傳輸大量數據(大量數據建議用接口方式獲取)
JS調Native
Native中通過引入官方提供的JavaScriptCore庫(iOS7以上),然後可以將api綁定到JSContext上(然後Html中JS默認通過window.top.*可調用)。
引入官方的庫文件
#import <JavaScriptCore/JavaScriptCore.h>
- 1
- 1
Native註冊api函數(OC)
-
-(void)webViewDidFinishLoad:(UIWebView *)webView{
-
[self hideProgress];
-
[self setJSInterface];
-
}
-
-(void)setJSInterface{
-
JSContext *context =[_wv valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
-
// 註冊名爲foo的api方法
-
context[@"foo"] = ^() {
-
//獲取參數
-
NSArray *args = [JSContext currentArguments];
-
NSString *title = [NSString stringWithFormat:@"%@",[args objectAtIndex:0]];
-
//做一些自己的邏輯
-
//返回一個值 'foo:'+title
-
return [NSString stringWithFormat:@"foo:%@", title];
-
};
-
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
Html中JS調用Native方法
window.top.foo('test');
- 1
- 1
說明:
- iOS7纔出現這種方式,在這之前,js無法直接調用Native,只能通過JSBridge方式簡介調用
- JS能調用到已經暴露的api,並且能得到相應返回值
- iOS原生本身是無法被JS調用的,但是通過引入官方提供的第三方”JavaScriptCore”,即可開放api給JS調用
JSBridge
什麼是JSBridge
JSBridge:聽其取名就是js和Native之前的橋樑,而實際上JSBridge確實是JS和Native之前的一種通信方式。簡單的說,JSBridge就是定義Native和JS的通信,Native只通過一個固定的橋對象調用JS,JS也只通過固定的橋對象調用Native。JSBridge另一個叫法及大家熟知的Hybrid app技術。
流程:H5->通過某種方式觸發一個url->Native捕獲到url,進行分析->原生做處理->Native調用H5的JSBridge對象傳遞迴調。
我們前面講過了原生的WebView/UIWebView控件已經能夠和Js實現數據通信了,那爲什麼還要JSBridge呢?
其實使用JSBridge有很多方面的考慮:
- Android4.2以下,addJavascriptInterface方式有安全漏掉
- iOS7以下,JS無法調用Native
- url scheme交互方式是一套現有的成熟方案,可以完美兼容各種版本,對以前老版本技術的兼容。
url scheme
url scheme是一種類似於url的鏈接,是爲了方便app直接互相調用設計的。具體來講如果是系統的url scheme,則打開系統應用,否則找看是否有app註冊這種scheme,打開對應app。
注:這種scheme必須原生app註冊後纔會生效。
而在我們實際的開發中,app不會註冊對應的scheme,而是由前端頁面通過某種方式觸發scheme(如用iframe.src),然後Native用某種方法捕獲對應的url觸發事件,然後拿到當前的觸發url,根據定義好的協議,分析當前觸發了那種方法。
JSBridge技術實現
要實現JSBridge,我們需要按以下步驟分析:
- 第一步:設計出一個Native與JS交互的全局橋對象
- 第二步:JS如何調用Native
- 第三步:Native如何得知api被調用
- 第四步:分析url-參數和回調的格式
- 第五步:Native如何調用JS
- 第六步:H5中api方法的註冊以及格式
JSBridge的完整流程可總結爲:
設計Native與JS交互的全局橋對象
我們規定,JS和Native之間的通信必須通過一個H5全局對象JSbridge來實現。該對象有如下特點:
該對象名爲”JSBridge”,是H5頁面中全局對象window的一個屬性,形如:
var JSBridge = window.JSBridge || (window.JSBridge = {});
- 1
- 1
該對象有如下方法:
- registerHandler( String,Function )H5調用註冊本地JS方法,註冊後Native可通過JSBridge調用。調用後會將方法註冊到本地變量messageHandlers 中。
- callHandler( String,JSON,Function )H5調用 調用原生開放的api,調用後實際上還是本地通過url scheme觸發。調用時會將回調id存放到本地變量responseCallbacks中
- _handleMessageFromNative( JSON )Native調用 原生調用H5頁面註冊的方法,或者通知H5頁面執行回調方法
JS調用Native
我們定義好了全局橋對象,可以通過它的callHandler方法來調用原生的api。
callHandler函數內部實現過程
在執行callHandler時,內部經歷了以下步驟:
- 判斷是否有回調函數,如果有,生成一個回調函數id,並將id和對應回調添加進入回調函數集合responseCallbacks中。
- 通過特定的參數轉換方法,將傳入的數據,方法名一起,拼接成一個url scheme
-
//url scheme的格式如
-
//基本有用信息就是後面的callbackId,handlerName與data
-
//原生捕獲到這個scheme後會進行分析
-
var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
- 使用內部早就創建好的一個隱藏iframe來觸發scheme
-
//創建隱藏iframe過程
-
var messagingIframe = document.createElement('iframe');
-
messagingIframe.style.display = 'none';
-
document.documentElement.appendChild(messagingIframe);
-
//觸發scheme
-
messagingIframe.src = uri;
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 1
- 2
- 3
- 4
- 5
- 6
- 7
注:正常來說是可以通過window.location.href達到發起網絡請求的效果的,但是有一個很嚴重的問題,就是如果我們連續多次修改window.location.href的值,在Native層只能接收到最後一次請求,前面的請求都會被忽略掉。所以JS端發起網絡請求的時候,需要使用iframe,這樣就可以避免這個問題。
Native通知api被調用
上一步,我們已經成功在H5頁面中觸發scheme,那麼Native如何捕獲scheme被觸發呢?
根據系統不同,Android和iOS分別有自己的處理方式。
Android
在Android中(WebViewClient裏),通過shouldoverrideurlloading可以捕獲到url scheme的觸發。
-
public boolean shouldOverrideUrlLoading(WebView view, String url){
-
//如果返回false,則WebView處理鏈接url,如果返回true,代表WebView根據程序來執行url
-
return true;
-
}
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
iOS
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後自行進行處理
- 1
- 2
- 3
- 4
- 5
- 1
- 2
- 3
- 4
- 5
分析url-參數和回調的格式
在前面的步驟中,Native已經接收到了JS調用的方法,那麼接下來,原生就應該按照定義好的數據格式來解析數據了,Native接收到Url後,可以按照這種格式將回調參數id、api名、參數提取出來,然後按如下步驟進行。
- 根據api名,在本地找尋對應的api方法,並且記錄該方法執行完後的回調函數id
- 根據提取出來的參數,根據定義好的參數進行轉化
- 原生本地執行對應的api功能方法
- 功能執行完畢後,找到這次api調用對應的回調函數id,然後連同需要傳遞的參數信息,組裝成一個JSON格式的參數
- 通過JSBridge通知H5頁面回調
Native調用JS
到了這一步,就該Native通過JSBridge調用H5的JS方法或者通知H5進行回調了。其中的messageJSON數據格式根據兩種不同的類型。
JSBridge._handleMessageFromNative(messageJSON);
- 1
- 1
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成功執行,並傳遞參數
H5中api方法的註冊以及格式
前面有提到Native主動調用H5中註冊的api方法,那麼h5中怎麼註冊供原生調用的api方法呢?
-
JSBridge.registerHandler('testH5Func',function(data,callback){
-
alert('測試函數接收到數據:'+JSON.stringify(data));
-
callback&&callback('測試回傳數據...');
-
});
- 1
- 2
- 3
- 4
- 1
- 2
- 3
- 4
如上代碼,其中第一個data即原生傳過來的數據,第二個callback是內部封裝過一次的,執行callback後會觸發url scheme,通知原生獲取回調信息.
完善JSBridge方案
github上有一個開源項目,它裏面的JSBridge做法在iOS上進一步優化了,所以參考他的做法,這裏進一步進行了完善。地址marcuswestin/WebViewJavascriptBridge
JSBridge對象圖解:
JSBridge實現完整流程:
總結
那麼我們在實際的開發中,如何針對Android和iOS的不同情況,統一出一種完整的方案。
另類實現:不採用url scheme方式
前面提到的JSBridge都是基於url scheme的,但其實如果不考慮Android4.2以下,iOS7以下,其實也可以用另一套方案的。
- Native調用JS的方法不變
- JS調用Native是不再通過觸發url scheme,而是採用自帶的交互
具體來講:
Android中,原生通過 addJavascriptInterface開放一個統一的api給JS調用,然後將觸發url scheme步驟變爲調用這個api,其餘步驟不變。
OS中,原生通過JavaScriptCore裏面的方法來註冊一個統一api,其餘和Android中一樣。