歡迎關注微信公衆號「隨手記技術團隊」,查看更多隨手記團隊的技術文章。轉載請註明出處
本文做者:譚海洋
原文連接:mp.weixin.qq.com/s/fKIyFhZC6…javascript
在移動開發中,開發的需求和節奏都愈來愈快,而Native App在這種節奏中略顯笨拙,開發週期長、用戶升級慢、應用市場審覈時間長都深受開發者弊病。而這時候不少開發者都提出了Hybrid App的概念,這種開發模式有着迭代靈活、多端統1、開發週期短、快速上線等優點。可是Hybrid App也有其不足的地方,在性能很難到達Native App的水平,在訪問設備上的硬件時也不是那麼駕輕就熟。對於這些問題,如今已經有較多的解決方案,比較重的框架有Facebook的React Native,輕量級別也有ionic。若是是已經成熟的產品,Web頁面較多遷移比較困難,也可使用VasSonic來提高WebView體驗,而後經過JS調用Native。目前公司項目中因爲歷史緣由採用後者的方式來實現,可是在使用過程當中因爲沒有統一的管理,存在了通信方式多樣、調用混亂和安全性差等幾個問題。下文主要講述如何經過從新設計JS調用框架來解決以上問題。前端
首先介紹一下WebView中JS和Native相互調用的方式、相互之間的差別。java
WebView調用JS有如下兩種方式:android
在API 19以前是隻能經過WebView.loadUrl()進行調用JavaScript。在API 19的時候新提供了WebView.evaluateJavascript(),它的運行效率會比loadUrl()高,還能夠傳入一個回調對象,方便獲取Web端的回傳信息。web
webView.evaluateJavascript("fromAndroid()", new ValueCallback<String>() {
@Override
public void onReceiveValue(String value) {
//do something
}
});
複製代碼
JS調用Native代碼有如下三種方式:安全
WebView.addJavascriptInterface()是官方推薦的作法,在默認狀況下WebView是關閉了JavaScript的調用,須要調用WebSetting.setJavaScriptEnabled(true)來進行開啓。這個方法須要一個Object類型的JavaScript Interface,而後經過@JavascriptInterface來標註提供的JS調用的方法,下面是一個Google官方提供的例子:bash
public class AppJavaScriptProxy {
private Activity activity = null;
public AppJavaScriptProxy(Activity activity) {
this.activity = activity;
}
@JavascriptInterface
public void showMessage(String message) {
Toast toast = Toast.makeText(this.activity.getApplicationContext(),
message,
Toast.LENGTH_SHORT);
toast.show();
}
}
複製代碼
webView.addJavascriptInterface(new AppJavaScriptProxy(this),「androidAppProxy」);
複製代碼
// JS代碼調用
if(typeof androidAppProxy !== "undefined"){
androidAppProxy.showMessage("Message from JavaScript");
} else {
alert("Running outside Android app");
}
複製代碼
這樣就能夠實現JS調用Android代碼,使用者只須要關注被JS調用方法的實現,對調用的過程是不可知的。使用的時候有幾個要注意的地方:微信
WebViewClient.shouldOverrideUrlLoading()是經過攔截Url的方式來實現與JS的交互。shouldOverrideUrlLoading()返回true時,表明攔截此次請求,讓咱們本身處理。shouldOverrideUrlLoading()返回false時,表明不攔截此次請求,讓WebView去處理此次請求。網絡
WebChromeClient.onJsAlert()、onJsConfirm()、onJsPrompt()三種方式和WebViewClient.shouldOverrideUrlLoading()相似,都是經過攔截請求的方式達到交互功能。app
總結:這三種方式實際上能夠概括成兩種:JavascriptInterface和攔截請求,二者之間各有好壞。
爲了解決前言中提到的通信方式多樣、調用混亂和安全性差等幾個問題,須要從新設計JS調用框架,將整個流程從WebView中剝離出來,達到低耦合的目的。綜合考慮後,決定沿用項目中以前的解決方式,經過攔截WebView請求的方式來實現。攔截性的方式在設計框架以前還須要考慮到通信協議的問題。
如上圖所示,經過設計通信協議達到多端統一通信。協議上面能夠參考現有的通信協議,或者根據項目需求和前端設計一套通用協議。這裏推薦一種簡單的現有的協議:統一資源標誌符。
jsbridge://method1?a=123&b=345&jsCall=jsMethod1" 複製代碼
該種標識容許用戶對網絡中(通常指萬維網)的資源經過特定的協議進行交互操做,在這裏不用徹底使用,只使用了其中的三個字段。scheme定義爲jsbridge,用於區分別的網絡請求。authority用來定義JS須要訪問的方法。後面的query用來傳參數,若是須要客戶端回調信息給前端,就能夠加個參數jsCall=jsMethod1,而後客戶端處理完後就能夠經過WebView進行回調。
WebView.loadUrl("javascript:jsMethod1(result=1)")
複製代碼
這樣就定義了一種簡單的交互方式,能讓JS和Native擁有基礎的交互能力。若是須要傳文件,能夠經過將文件流轉成Base64而後在通信,固然若是文件太大,這種方式會有內存方面的風險。這裏還有另一種方式,攔截WebView的資源請求,將文件以流的形式進行通信:
webView.setWebViewClient(new WebViewClient(){
@Override
public WebResourceResponse shouldInterceptRequest(WebView view, String url) {
return new WebResourceResponse("image/jpeg", "UTF-8", new FileInputStream(new File("xxxx");
}
}
複製代碼
在設計中,主要考慮瞭如下幾點:
經過分析整個通信的流程,結合項目中的須要,大致抽象出通信流程中的五個角色:
這樣的模式和系統提供的JavascriptInterface方式基本一致,可是咱們能夠作的事情比JavascriptInterface方式更多,並且整個系統解耦清晰,可是這個結構實際上還缺少較多的東西,沒法達到設計的目標,整個流程中缺少擴展性,沒有攔截和二次處理機制。
能夠在執行JsMethod以前添加一個攔截器,加強擴展性。
安全性方面也能夠經過添加攔截器的方式來實現,將JS請求攔截在執行JsMethod以前,而每一個JsMethod的安全級別能夠經過擴展註解參數來標註。例以下面代碼,添加permission字段來標示方法的安全級別。
@JsMethod(permission = "high")
public void requestInfo(IJsCall jsCall) {
// do something
}
複製代碼
框架骨架搭建好了後,還須要一些優化性的設計:
###實現效果
最後,框架大致設計完畢,實現都是比較簡單的。如今來看看使用的時候,首先是JS發起一個請求:
var iframe = document.createElement('iframe');
iframe.setAttribute('src', 'jsbridge://method1?a=123&b=345');
document.body.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
複製代碼
客戶端只須要簡單的對WebView的請求作攔截。
@Override
public boolean shouldOverrideUrlLoading(WebView webView, String url) {
boolean handle = super.shouldOverrideUrlLoading(webView, url);
if (!handle) {
handle = JSBridge.parse(activity, webView, url);
}
return handle;
}
複製代碼
建立一個解析當前協議的對象,這個是之後均可以複用的:
public class JsProcessor implements IProcessor {
public static final int TYPE_PROCESSOR = 1;
/**
* 協議編號
* @return
*/
@Override
public int getType() {
return TYPE_PROCESSOR;
}
/**
* 判斷請求是否是屬於這個協議
* @param url
* @return
*/
@Override
public boolean isProtocol(String url) {
return !TextUtils.isEmpty(url) && url.startsWith("jsbridge");
}
/**
* 解析協議
* @param context
* @param webView
* @param url
* @param webViewContext WebView的環境
* @return
*/
@Override
public IJsCall parse(Context context, WebView webView, final String url, Object webViewContext) {
return new IJsCall<RequestBean, ResponseBean>() {
private String mMethodName;
@Override
public void callback(ResponseBean data, WebView webView) {
JSBridge.callback(data, webView);
}
@Override
public String url() {
return null;
}
@Override
public RequestBean parseData() {
if (TextUtils.isEmpty(url)) {
return null;
}
Uri uri = Uri.parse(url);
String methodName = uri.getPath();
methodName = methodName.replace("/", "");
mMethodName = methodName;
return new RequsetBean(url);
}
@Override
public String method() {
return mMethodName;
}
};
}
}
複製代碼
建立一個提供JS方法的對象,在對外提供的方法上加入註解@JsMethod,並標註調用該方法的協議編號、方法名稱和權限級別,方法中所須要的信息都經過IJsCall獲取,處理完成後,經過IJsCall回調信息給JS。
public class JsProvider {
@JsMethod(processorType = JsProcessor.TYPE_PROCESSOR, name = "method1", permission = "high")
public void method1(IJsCall jsCall) {
// do anything
// ...
// ...
// ...
jsCall.callback("xxxx");
}
}
複製代碼
以上就完成了一次JS和Native的通信。整個通信的細節不對外開放,使用者只用關注方法的開發,方法的信息經過註解來承載,解析註解時能夠經過編譯時生成代碼來提升效率。白名單和數據加密直接經過攔截器來實現。整個系統完美的解決了以前項目中問題,並且也方便之後的業務發展。
Hybrid App是之後的趨勢,JS和Native之間業務邏輯也會愈來愈重,因此項目中這塊的設計也很是重要,須要不斷的根據業務來調整,保證其穩定性的同時,又有很強的擴展能力。