作過混合開發的不少人都知道Ionic和PhoneGap之類的框架,這些框架在web基礎上包了一層Native,而後經過Bridge技術使得js能夠調用視頻、位置、音頻等功能。本文就是介紹這層Bridge的交互原理,經過閱讀本文你能夠了解到js與ios及android底層的通信原理及JSBridge的封裝技術及調試方法。javascript
下面分別介紹IOS和Android與Javascript的底層交互原理java
在講解原理以前,首先來了解下iOS的UIWebView組件,先來看一下蘋果官方的介紹:android
You can use the UIWebView class to embed web content in your application. To do so, you simply create a UIWebView object, attach it to a window, and send it a request to load web content. You can also use this class to move back and forward in the history of webpages, and you can even set some web content properties programmatically.ios
上面的意思是說UIWebView是一個可加載網頁的對象,它有瀏覽記錄功能,且對加載的網頁內容是可編程的。說白了UIWebView有相似瀏覽器的功能,咱們使用能夠它來打開頁面,並作一些定製化的功能,如可讓js調某個方法能夠取到手機的GPS信息。git
但須要注意的是,Safari瀏覽器使用的瀏覽器控件和UIwebView組件並非同一個,二者在性能上有很大的差距。幸運的是,蘋果發佈iOS8的時候,新增了一個WKWebView組件,若是你的APP只考慮支持iOS8及以上版本,那麼你就可使用這個新的瀏覽器控件了。github
原生的UIWebView類提供了下面一些屬性和方法web
屬性:objective-c
方法:編程
Native調用Javascript語言,是經過UIWebView
組件的stringByEvaluatingJavaScriptFromString
方法來實現的,該方法返回js腳本的執行結果。swift
// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];複製代碼
從上面代碼能夠看出它其實就是調用了window
下的一個對象,若是咱們要讓native來調用咱們js寫的方法,那這個方法就要在window
下能訪問到。但從全局考慮,咱們只要暴露一個對象如JSBridge對native調用就行了,因此在這裏能夠對native的代碼作一個簡單的封裝:
//下面爲僞代碼
webview.setDataToJs(somedata);
webview.setDataToJs = function(data) {
webview.stringByEvaluatingJavaScriptFromString("JSBridge.trigger(event, data)")
}複製代碼
反過來,Javascript調用Native,並無現成的API能夠直接拿來用,而是須要間接地經過一些方法來實現。UIWebView有個特性:在UIWebView內發起的全部網絡請求,均可以經過delegate函數在Native層獲得通知。這樣,咱們就能夠在UIWebView內發起一個自定義的網絡請求,一般是這樣的格式:jsbridge://methodName?param1=value1¶m2=value2
因而在UIWebView的delegate函數中,咱們只要發現是jsbridge://開頭的地址,就不進行內容的加載,轉而執行相應的調用邏輯。
發起這樣一個網絡請求有兩種方式:1. 經過localtion.href;2. 經過iframe方式;
經過location.href有個問題,就是若是咱們連續屢次修改window.location.href的值,在Native層只能接收到最後一次請求,前面的請求都會被忽略掉。
使用iframe方式,以喚起Native APP的分享組件爲例,簡單的封閉以下:
var url = 'jsbridge://doAction?title=分享標題&desc=分享描述&link=http%3A%2F%2Fwww.baidu.com';
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
iframe.remove();
}, 100);複製代碼
而後Webview就能夠攔截這個請求,而且解析出相應的方法和參數。以下代碼所示:
func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
print("shouldStartLoadWithRequest")
let url = request.URL
let scheme = url?.scheme
let method = url?.host
let query = url?.query
if url != nil && scheme == "jsbridge" {
print("scheme == \(scheme)")
print("method == \(method)")
print("query == \(query)")
switch method! {
case "getData":
self.getData()
case "putData":
self.putData()
case "gotoWebview":
self.gotoWebview()
case "gotoNative":
self.gotoNative()
case "doAction":
self.doAction()
case "configNative":
self.configNative()
default:
print("default")
}
return false
} else {
return true
}
}複製代碼
在android中,native與js的通信方式與ios相似,ios中的經過schema方式在android中也是支持的。
目前在android中有三種調用native的方式:
1.經過schema方式,使用shouldOverrideUrlLoading
方法對url協議進行解析。這種js的調用方式與ios的同樣,使用iframe來調用native代碼。
2.經過在webview頁面裏直接注入原生js代碼方式,使用addJavascriptInterface
方法來實現。
在android裏實現以下:
class JSInterface {
@JavascriptInterface //注意這個代碼必定要加上
public String getUserData() {
return "UserData";
}
}
webView.addJavascriptInterface(new JSInterface(), "AndroidJS");複製代碼
上面的代碼就是在頁面的window對象裏注入了AndroidJS
對象。在js裏能夠直接調用
alert(AndroidJS.getUserData()) //UserDate複製代碼
3.使用prompt,console.log,alert方式,這三個方法對js裏是屬性原生的,在android webview這一層是能夠重寫這三個方法的。通常咱們使用prompt,由於這個在js裏使用的很少,用來和native通信反作用比較少。
class YouzanWebChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
// 這裏就能夠對js的prompt進行處理,經過result返回結果
}
@Override
public boolean onConsoleMessage(ConsoleMessage consoleMessage) {
}
@Override
public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
}
}複製代碼
在android裏是使用webview的loadUrl
進行調用的,如:
// 調用js中的JSBridge.trigger方法
webView.loadUrl("javascript:JSBridge.trigger('webviewReady')");複製代碼
上面咱們瞭解了js與native通信的底層原理,因此咱們能夠封裝一個基礎的通信方法doCall
來屏蔽android與ios的差別。
YouzanJsBridge = {
doCall: function(functionName, data, callback) {
var _this = this;
// 解決連續調用問題
if (this.lastCallTime && (Date.now() - this.lastCallTime) < 100) {
setTimeout(function() {
_this.doCall(functionName, data, callback);
}, 100);
return;
}
this.lastCallTime = Date.now();
data = data || {};
if (callback) {
$.extend(data, { callback: callback });
}
if (UA.isIOS()) {
$.each(data, function(key, value) {
if ($.isPlainObject(value) || $.isArray(value)) {
data[key] = JSON.stringify(value);
}
});
var url = Args.addParameter('youzanjs://' + functionName, data);
var iframe = document.createElement('iframe');
iframe.style.width = '1px';
iframe.style.height = '1px';
iframe.style.display = 'none';
iframe.src = url;
document.body.appendChild(iframe);
setTimeout(function() {
iframe.remove();
}, 100);
} else if (UA.isAndroid()) {
window.androidJS && window.androidJS[functionName] && window.androidJS[functionName](JSON.stringify(data));
} else {
console.error('未獲取platform信息,調取api失敗');
}
}
}複製代碼
上面android端咱們使用了addJavascriptInterface方法來注入一個AndroidJS對象。
在項目的實踐中,咱們逐漸抽象出一些通用的方法,這些方法基本上都是能夠知足項目的需求。以下所示:
使用場景:H5須要從Native APP獲取某些數據的時候,能夠調用這個方法。
參數 | 類型 | 是否必須 | 示例值 | 說明 |
---|---|---|---|---|
datatype | String | 是 | userInfo | 數據類型 |
callback | Function | 是 | 回調函數 | |
extra | Object | 否 | 傳遞給Native APP的數據對象 |
示例代碼:
JSBridge.getData('userInfo',function(data) {
console.log(data);
});複製代碼
使用場景:H5告訴Native APP一些數據,能夠調用這個方法。
參數 | 類型 | 是否必須 | 示例值 | 說明 |
---|---|---|---|---|
datatype | String | 是 | userInfo | 數據類型 |
data | Object | 是 | { username: 'zhangsan', age: 20 } | 傳遞給Native APP的數據對象 |
示例代碼:
JSBridge.putData('userInfo', {
username: 'zhangsan',
age: 20
});複製代碼
參數 | 類型 | 是否必須 | 示例值 | 說明 |
---|---|---|---|---|
url | String | 是 | www.youzan.com | 網頁連接地址,通常都只要傳遞URL參數就能夠了 |
page | String | 否 | web | 網頁page類型,默認爲web |
data | Object | 否 | 額外參數對象 |
示例代碼:
// 示例1:打開一個網頁
JSBridge.gotoWebview('http://www.youzan.com');
// 示例2:打開一個網頁,而且傳遞額外的參數給Native APP
JSBridge.gotoWebview('http://www.youzan.com', 'goodsDetail', {
goods_id: 10000,
title: '這是商品的標題',
desc: '這是商品的描述'
});複製代碼
參數 | 類型 | 是否必須 | 示例值 | 說明 |
---|---|---|---|---|
page | String | 是 | loginPage | Native頁面標示符,例如loginPage |
data | Object | 否 | { username: 'zhangsan', age: 20 } | 額外參數對象 |
示例代碼:
// 示例1:打開Native APP登陸頁面
JSBridge.gotoNative('loginPage');
// 示例2:打開Native APP登陸頁面,而且傳遞用戶名給Native APP
JSBridge.gotoNative('loginPage', {
username: '張三'
});複製代碼
參數 | 類型 | 是否必須 | 示例值 | 說明 |
---|---|---|---|---|
action | String | 是 | copy | 操做功能類型,例如分享、複製 |
data | Object | 否 | { content: '這是要複製的內容' } | 額外參數 |
示例代碼:
// 示例1:調用Native APP複製一段文本到剪切板
JSBridge.doAction('copy', {
content: '這是要複製的內容'
});
// 示例2:調用Native APP的分享組件,分享當前網頁到微信
JSBridge.doAction('share', {
title: '分享標題',
desc: '分享描述',
link: 'http://www.youzan.com',
imgs_url: 'http://wap.koudaitong.com/v2/common/url/create?type=homepage&index%2Findex=&kdt_id=63077&alias=63077'
});複製代碼
(1)首先須要打開Safari的調試模式,在Safari的菜單中,選擇「Safari」→「Preference」→「Advanced」,勾選上「Show Develop menu in menu bar」選項,以下圖所示。
(4)Safari鏈接上UIWebView以後,咱們就能夠直接在Safari中直接修改HTML、CSS,以及調試Javascript。
本文由 @kk @勁風 共同創做,首發於有贊技術博客: tech.youzan.com/jsbridge/