javascript
jsbridge实现及原理_JSBridge 实现原理解析
JSBridge 項目以 js 與 android 通信為例,講解 JSBridge 實現原理,下面提到的方法在 iOS(UIWebview 或 WKWebview)均有對應方法。
1. native to js
兩種 native 調用 js 方法,注意被調用的方法需要在 JS 全局上下文上
loadUrl
evaluateJavascript
1.1 loadUrlmWebview.loadUrl("javascript: func()");
1.2 evaluateJavascriptmWebview.evaluateJavascript("javascript: func()", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
return;
}
});
上述兩種 native 調用 js 的方式對比如下表:方式優點缺點loadUrl兼容性好1. 會刷新頁面 2. 無法獲取 js 方法執行結果
evaluateJavascript1. 性能好 2. 可獲取 js 執行后的返回值僅在安卓 4.4 以上可用
2. js to native
三種 js 調用 native 方法
攔截 Url Schema(假請求)
攔截 prompt alert confirm
注入 JS 上下文
2.1 攔截 Url Schema
即由 h5 發出一條新的跳轉請求,native 通過攔截 URL 獲取 h5 傳過來的數據。
跳轉的目的地是一個非法不存在的 URL 地址,例如:"jsbridge://methodName?{"data": arg, "cbName": cbName}"
具體示例如下:"jsbridge://openScan?{"data": {"scanType": "qrCode"}, "cbName": "handleScanResult"}"
h5 和 native 約定一個通信協議,例如 jsbridge, 同時約定調用 native 的方法名 methodName 作為域名,以及后面帶上調用該方法的參數 arg,和接收該方法執行結果的 js 方法名 cbName。
具體可以在 js 端封裝相關方法,供業務端統一調用,代碼如下:window.callbackId = 0;
function callNative(methodName, arg, cb) {
const args = {
data: arg === undefined ? null : JSON.stringify(arg),
};
if (typeof cb === 'function') {
const cbName = 'CALLBACK' + window.callbackId++;
window[cbName] = cb;
args['cbName'] = cbName;
}
const url = 'jsbridge://' + methodName + '?' + JSON.stringify(args);
...
}
以上封裝中較為巧妙的是將用于接收 native 執行結果的 js 回調方法 cb 掛載到 window 上,并為防止命名沖突,通過全局的 callbackId 來區分,然后將該回調函數在 window 上的名字放在參數中傳給 native 端。native 拿到 cbName 后,執行完方法后,將執行結果通過 native 調用 js 的方式(上面提到的兩種方法),調用 cb 傳給 h5 端(例如將掃描結果傳給 h5)。
至于如何在 h5 中發起請求,可以設置 window.location.href 或者創建一個新的 iframe 進行跳轉。function callNative(methodName, arg, cb) {
...
const url = 'jsbridge://' + method + '?' + JSON.stringify(args);
// 通過 location.href 跳轉
window.location.href = url;
// 通過創建新的 iframe 跳轉
const iframe = document.createElement('iframe');
iframe.src = url;
iframe.style.width = 0;
iframe.style.height = 0;
document.body.appendChild(iframe);
window.setTimeout(function() {
document.body.removeChild(iframe);
}, 800);
}
native 會攔截 h5 發出的請求,當檢測到協議為 jsbridge 而非普通的 http/https/file 等協議時,會攔截該請求,解析出 URL 中的 methodName、arg、 cbName,執行該方法并調用 js 回調函數。
下面以安卓為例,通過覆蓋 WebViewClient 類的 shouldOverrideUrlLoading 方法進行攔截,android 端具體封裝會在下面單獨的板塊進行說明。import android.util.Log;
import android.webkit.WebView;
import android.webkit.WebViewClient;
public class JSBridgeViewClient extends WebViewClient {
@Override
public boolean shouldOverrideUrlLoading(WebView view, String url) {
JSBridge.call(view, url);
return true;
}
}
攔截 URL Schema 的問題連續發送時消息丟失
如下代碼:window.location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";
window.location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";
js 此時的訴求是在同一個運行邏輯內,快速的連續發送出 2 個通信請求,用客戶端本身 IDE 的 log,按順序打印 111,222,那么實際結果是 222 的通信消息根本收不到,直接會被系統拋棄丟掉。
原因:因為 h5 的請求歸根結底是一種模擬跳轉,跳轉這件事情上 webview 會有限制,當 h5 連續發送多條跳轉的時候,webview 會直接過濾掉后發的跳轉請求,因此第二個消息根本收不到,想要收到怎么辦?js 里將第二條消息延時一下。//發第一條消息
location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";
//延時發送第二條消息
setTimeout(500,function(){
location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";
});
但這并不能保證此時是否有其他地方通過這種方式進行請求,為系統解決此問題,js 端可以封裝一層隊列,所有 js 代碼調用消息都先進入隊列并不立刻發送,然后 h5 會周期性比如 500 毫秒,清空一次隊列,保證在很快的時間內絕對不會連續發 2 次請求通信。URL 長度限制
如果需要傳輸的數據較長,例如方法參數很多時,由于 URL 長度限制,仍以丟失部分數據。
2.2 攔截 prompt alert confirm
即由 h5 發起 alert confirm prompt,native 通過攔截 prompt 等獲取 h5 傳過來的數據。
因為 alert confirm 比較常用,所以一般通過 prompt 進行通信。
約定的傳輸數據的組合方式以及 js 端封裝方法的可以類似上面的 攔截 URL Schema 提到的方式。function callNative(methodName, arg, cb) {
...
const url = 'jsbridge://' + method + '?' + JSON.stringify(args);
prompt(url);
}
native 會攔截 h5 發出的 prompt,當檢測到協議為 jsbridge 而非普通的 http/https/file 等協議時,會攔截該請求,解析出 URL 中的 methodName、arg、 cbName,執行該方法并調用 js 回調函數。
下面以安卓為例,通過覆蓋 WebChromeClient 類的 onJsPrompt 方法進行攔截,android 端具體封裝會在下面單獨的板塊進行說明。import android.webkit.JsPromptResult;
import android.webkit.WebChromeClient;
import android.webkit.WebView;
public class JSBridgeChromeClient extends WebChromeClient {
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
result.confirm(JSBridge.call(view, message));
return true;
}
}
這種方式沒有太大缺點,也不存在連續發送時信息丟失。不過 iOS 的 UIWebView 不支持該方式(WKWebView 支持)。
2.3 注入 JS 上下文
即由 native 將實例對象通過 webview 提供的方法注入到 js 全局上下文,js 可以通過調用 native 的實例方法來進行通信。
具體有安卓 webview 的 addJavascriptInterface,iOS UIWebview 的 JSContext,iOS WKWebview 的 scriptMessageHandler。
下面以安卓 webview 的 addJavascriptInterface 為例進行講解。
首先 native 端注入實例對象到 js 全局上下文,代碼大致如下,具體封裝會在下面的單獨板塊進行講解:public class MainActivity extends AppCompatActivity {
private WebView mWebView;
@Override
protected void onCreate(Bundle savedInstanceState) {
super.onCreate(savedInstanceState);
setContentView(R.layout.activity_main);
mWebView = (WebView) findViewById(R.id.mWebView);
...
// 將 NativeMethods 類下面的提供給 js 的方法轉換成 hashMap
JSBridge.register("JSBridge", NativeMethods.class);
// 將 JSBridge 的實例對象注入到 js 全局上下文中,名字為 _jsbridge,該實例對象下有 call 方法
mWebView.addJavascriptInterface(new JSBridge(mWebView), "_jsbridge");
}
}
public class NativeMethods {
// 用來供 js 調用的方法
public static void methodName(WebView view, JSONObject arg, CallBack callBack) {
}
}
public class JSBridge {
private WebView mWebView;
public JSBridge(WebView webView) {
this.mWebView = webView;
}
private static Map> exposeMethods = new HashMap<>();
// 靜態方法,用于將傳入的第二個參數的類下面用于提供給 javacript 的接口轉成 Map,名字為第一個參數
public static void register(String exposeName, Class> classz) {
...
}
// 實例方法,用于提供給 js 統一調用的方法
@JavascriptInterface
public String call(String methodName, String args) {
...
}
}
然后 h5 端可以在 js 調用 window._jsbridge 實例下面的 call 方法,傳入的數據組合方式可以類似上面兩種方式。具體代碼如下:window.callbackId = 0;
function callNative(method, arg, cb) {
let args = {
data: arg === undefined ? null : JSON.stringify(arg)
};
if (typeof cb === 'function') {
const cbName = 'CALLBACK' + window.callbackId++;
window[cbName] = cb;
args['cbName'] = cbName;
}
if (window._jsbridge) {
window._jsbridge.call(method, JSON.stringify(args));
}
}
注入 JS 上下文的問題
以安卓 webview 的 addJavascriptInterface 為例,在安卓 4.2 版本之前,js 可以利用 java 的反射 Reflection API,取得構造該實例對象的類的內部信息,并能直接操作該對象的內部屬性及方法,這種方式會造成安全隱患,例如如果加載了外部網頁,該網頁的惡意 js 腳本可以獲取手機的存儲卡上的信息。
在安卓 4.2 版本后,可以通過在提供給 js 調用的 java 方法前加裝飾器 @JavascriptInterface,來表明僅該方法可以被 js 調用。
上述三種 js 調用 native 的方式對比如下表:方式優點缺點攔截 Url Schema(假請求)無安全漏洞1. 連續發送時消息丟失 2. Url 長度限制,傳輸數據大小受限
攔截 prompt alert confirm無安全漏洞iOS 的 UIWebView 不支持該方式
注入 JS 上下文官方提供,方便簡捷在安卓 4.2 以下有安全漏洞
3. 安卓端 java 的封裝
native 與 h5 交互部分的代碼在上面已經提到了,這里主要是講述 native 端如何封裝暴露給 h5 的方法。
首先單獨封裝一個類 NativeMethods,將供 h5 調用的方法以公有且靜態方法的形式寫入。如下:public class NativeMethods {
public static void showToast(WebView view, JSONObject arg, CallBack callBack) {
...
}
}
接下來考慮如何在 NativeMethods 和 h5 之前建立一個橋梁,JSBridge 類因運而生。
JSBridge 類下主要有兩個靜態方法 register 和 call。其中 register 方法是用來將供 h5 調用的方法轉化成 Map 形式,以便查詢。而 call 方法主要是用接收 h5 端的調用,分解 h5 端傳來的參數,查找并調用 Map 中的對應的 Native 方法。
JSBridge 類的靜態方法 register
首先在 JSBridge 類下聲明一個靜態屬性 exposeMethods,數據類型為 HashMap 。然后聲明靜態方法 register,參數有字符串 exposeName 和類 classz,將 exposeName 和 classz 的所有靜態方法 組合成一個 map,例如:{
jsbridge: {
showToast: ...
openScan: ...
}
}
代碼如下:private static Map> exposeMethods = new HashMap<>();
public static void register(String exposeName, Class> classz) {
if (!exposeMethods.containsKey(exposeName)) {
exposeMethods.put(exposeName, getAllMethod(classz));
}
}
由上可知我們需要定義一個 getAllMethod 方法用來將類里的方法轉化為 HashMap 數據格式。在該方法里同樣聲明一個 HashMap,并將滿足條件的方法轉化成 Map,key 為方法名,value 為方法。
其中條件為 公有 public 靜態 static 方法且第一個參數為 Webview 類的實例,第二個參數為 JSONObject 類的實例,第三個參數為 CallBack 類的實例。 (CallBack 是自定義的類,后面會講到)
代碼如下:private static HashMap getAllMethod(Class injectedCls) {
HashMap methodHashMap = new HashMap<>();
Method[] methods = injectedCls.getDeclaredMethods();
for (Method method: methods) {
if(method.getModifiers()!=(Modifier.PUBLIC | Modifier.STATIC) || method.getName()==null) {
continue;
}
Class[] parameters = method.getParameterTypes();
if (parameters!=null && parameters.length==3) {
if (parameters[0] == WebView.class && parameters[1] == JSONObject.class && parameters[2] == CallBack.class) {
methodHashMap.put(method.getName(), method);
}
}
}
return methodHashMap;
}
JSBridge 類的靜態方法 call
由于注入 JS 上下文和兩外兩種,h5 端傳過來的參數形式不同,所以處理參數的方式略有不同。
下面以攔截 Prompt 的方式為例進行講解,在該方式中 call 接收的第一個參數為 webView,第二個參數是 arg,即 h5 端傳過來的參數。還記得攔截 Prompt 方式時 native 端和 h5 端約定的傳輸數據的方式么?"jsbridge://openScan?{"data": {"scanType": "qrCode"}, "cbName":"handleScanResult"}"
call 方法首先會判斷字符串是否以 jsbridge 開頭(native 端和 h5 端之間約定的傳輸數據的協議名),然后該字符串轉成 Uri 格式,然后獲取其中的 host 名,即方法名,獲取 query,即方法參數和 js 回調函數名組合的對象。最后查找 exposeMethods 的映射,找到對應的方法并執行該方法。public static String call(WebView webView, String urlString) {
if (!urlString.equals("") && urlString!=null && urlString.startsWith("jsbridge")) {
Uri uri = Uri.parse(urlString);
String methodName = uri.getHost();
try {
JSONObject args = new JSONObject(uri.getQuery());
JSONObject arg = new JSONObject(args.getString("data"));
String cbName = args.getString("cbName");
if (exposeMethods.containsKey("JSBridge")) {
HashMap methodHashMap = exposeMethods.get("JSBridge");
if (methodHashMap!=null && methodHashMap.size()!=0 && methodHashMap.containsKey(methodName)) {
Method method = methodHashMap.get(methodName);
if (method!=null) {
method.invoke(null, webView, arg, new CallBack(webView, cbName));
}
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
return null;
}
CallBack 類
js 調用 native 方法成功后,native 有必要返回給 js 一些反饋,例如接口是否調用成功,或者 native 執行后的得到的數據(例如掃碼)。所以 native 需要執行 js 回調函數。
執行 js 回調函數方式本質是 native 調用 h5 的 js 方法,方式仍舊是上面提到的兩種方式 evaluateJavascript 和 loadUrl。簡單來說可以直接將 js 的回調函數名傳給對應的 native 方法,native 執行通過 evaluateJavascript 調用。
但為了統一封裝調用回調的方式,我們可以定義一個 CallBack 類,在其中定義一個名為 apply 的靜態方法,該方法直接調用 js 回調。
注意:native 執行 js 方法需要在主線程上。public class CallBack {
private String cbName;
private WebView mWebView;
public CallBack(WebView webView, String cbName) {
this.cbName = cbName;
this.mWebView = webView;
}
public void apply(JSONObject jsonObject) {
if (mWebView!=null) {
mWebView.post(() -> {
mWebView.evaluateJavascript("javascript:" + cbName + "(" + jsonObject.toString() + ")", new ValueCallback() {
@Override
public void onReceiveValue(String value) {
return;
}
});
});
}
}
}
到此為止 JSBridge 的大致原理都講完了。但功能仍可再加完善,例如:
native 執行 js 方法時,可接受 js 方法中異步返回的數據,比如在 js 方法中請求某個接口在返回數據。直接調用 webview 提供的 evaluateJavascript,在第二個參數的類 ValueCallback 的實例方法 onReceiveValue 并不能接收到 js 異步返回的數據。
后面有空 native 調用 js 方式會繼續完善的,最后以一句古語互勉:
路漫漫其修遠兮 吾將上下而求索
總結
以上是生活随笔為你收集整理的jsbridge实现及原理_JSBridge 实现原理解析的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 马斯克:SpaceX“星舰”发射推迟至周
- 下一篇: linux启动mqtt_linux下安装