javascript
jsbridge实现及原理_JSBridge 实现原理解析
JSBridge 項(xiàng)目以 js 與 android 通信為例,講解 JSBridge 實(shí)現(xiàn)原理,下面提到的方法在 iOS(UIWebview 或 WKWebview)均有對(duì)應(yīng)方法。
1. native to js
兩種 native 調(diào)用 js 方法,注意被調(diào)用的方法需要在 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 調(diào)用 js 的方式對(duì)比如下表:方式優(yōu)點(diǎn)缺點(diǎn)loadUrl兼容性好1. 會(huì)刷新頁(yè)面 2. 無(wú)法獲取 js 方法執(zhí)行結(jié)果
evaluateJavascript1. 性能好 2. 可獲取 js 執(zhí)行后的返回值僅在安卓 4.4 以上可用
2. js to native
三種 js 調(diào)用 native 方法
攔截 Url Schema(假請(qǐng)求)
攔截 prompt alert confirm
注入 JS 上下文
2.1 攔截 Url Schema
即由 h5 發(fā)出一條新的跳轉(zhuǎn)請(qǐng)求,native 通過(guò)攔截 URL 獲取 h5 傳過(guò)來(lái)的數(shù)據(jù)。
跳轉(zhuǎn)的目的地是一個(gè)非法不存在的 URL 地址,例如:"jsbridge://methodName?{"data": arg, "cbName": cbName}"
具體示例如下:"jsbridge://openScan?{"data": {"scanType": "qrCode"}, "cbName": "handleScanResult"}"
h5 和 native 約定一個(gè)通信協(xié)議,例如 jsbridge, 同時(shí)約定調(diào)用 native 的方法名 methodName 作為域名,以及后面帶上調(diào)用該方法的參數(shù) arg,和接收該方法執(zhí)行結(jié)果的 js 方法名 cbName。
具體可以在 js 端封裝相關(guān)方法,供業(yè)務(wù)端統(tǒng)一調(diào)用,代碼如下: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 執(zhí)行結(jié)果的 js 回調(diào)方法 cb 掛載到 window 上,并為防止命名沖突,通過(guò)全局的 callbackId 來(lái)區(qū)分,然后將該回調(diào)函數(shù)在 window 上的名字放在參數(shù)中傳給 native 端。native 拿到 cbName 后,執(zhí)行完方法后,將執(zhí)行結(jié)果通過(guò) native 調(diào)用 js 的方式(上面提到的兩種方法),調(diào)用 cb 傳給 h5 端(例如將掃描結(jié)果傳給 h5)。
至于如何在 h5 中發(fā)起請(qǐng)求,可以設(shè)置 window.location.href 或者創(chuàng)建一個(gè)新的 iframe 進(jìn)行跳轉(zhuǎn)。function callNative(methodName, arg, cb) {
...
const url = 'jsbridge://' + method + '?' + JSON.stringify(args);
// 通過(guò) location.href 跳轉(zhuǎn)
window.location.href = url;
// 通過(guò)創(chuàng)建新的 iframe 跳轉(zhuǎn)
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 會(huì)攔截 h5 發(fā)出的請(qǐng)求,當(dāng)檢測(cè)到協(xié)議為 jsbridge 而非普通的 http/https/file 等協(xié)議時(shí),會(huì)攔截該請(qǐng)求,解析出 URL 中的 methodName、arg、 cbName,執(zhí)行該方法并調(diào)用 js 回調(diào)函數(shù)。
下面以安卓為例,通過(guò)覆蓋 WebViewClient 類的 shouldOverrideUrlLoading 方法進(jìn)行攔截,android 端具體封裝會(huì)在下面單獨(dú)的板塊進(jìn)行說(shuō)明。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 的問(wèn)題連續(xù)發(fā)送時(shí)消息丟失
如下代碼:window.location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";
window.location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";
js 此時(shí)的訴求是在同一個(gè)運(yùn)行邏輯內(nèi),快速的連續(xù)發(fā)送出 2 個(gè)通信請(qǐng)求,用客戶端本身 IDE 的 log,按順序打印 111,222,那么實(shí)際結(jié)果是 222 的通信消息根本收不到,直接會(huì)被系統(tǒng)拋棄丟掉。
原因:因?yàn)?h5 的請(qǐng)求歸根結(jié)底是一種模擬跳轉(zhuǎn),跳轉(zhuǎn)這件事情上 webview 會(huì)有限制,當(dāng) h5 連續(xù)發(fā)送多條跳轉(zhuǎn)的時(shí)候,webview 會(huì)直接過(guò)濾掉后發(fā)的跳轉(zhuǎn)請(qǐng)求,因此第二個(gè)消息根本收不到,想要收到怎么辦?js 里將第二條消息延時(shí)一下。//發(fā)第一條消息
location.href = "jsbridge://callNativeNslog?{"data": "111", "cbName": ""}";
//延時(shí)發(fā)送第二條消息
setTimeout(500,function(){
location.href = "jsbridge://callNativeNslog?{"data": "222", "cbName": ""}";
});
但這并不能保證此時(shí)是否有其他地方通過(guò)這種方式進(jìn)行請(qǐng)求,為系統(tǒng)解決此問(wèn)題,js 端可以封裝一層隊(duì)列,所有 js 代碼調(diào)用消息都先進(jìn)入隊(duì)列并不立刻發(fā)送,然后 h5 會(huì)周期性比如 500 毫秒,清空一次隊(duì)列,保證在很快的時(shí)間內(nèi)絕對(duì)不會(huì)連續(xù)發(fā) 2 次請(qǐng)求通信。URL 長(zhǎng)度限制
如果需要傳輸?shù)臄?shù)據(jù)較長(zhǎng),例如方法參數(shù)很多時(shí),由于 URL 長(zhǎng)度限制,仍以丟失部分?jǐn)?shù)據(jù)。
2.2 攔截 prompt alert confirm
即由 h5 發(fā)起 alert confirm prompt,native 通過(guò)攔截 prompt 等獲取 h5 傳過(guò)來(lái)的數(shù)據(jù)。
因?yàn)?alert confirm 比較常用,所以一般通過(guò) prompt 進(jìn)行通信。
約定的傳輸數(shù)據(jù)的組合方式以及 js 端封裝方法的可以類似上面的 攔截 URL Schema 提到的方式。function callNative(methodName, arg, cb) {
...
const url = 'jsbridge://' + method + '?' + JSON.stringify(args);
prompt(url);
}
native 會(huì)攔截 h5 發(fā)出的 prompt,當(dāng)檢測(cè)到協(xié)議為 jsbridge 而非普通的 http/https/file 等協(xié)議時(shí),會(huì)攔截該請(qǐng)求,解析出 URL 中的 methodName、arg、 cbName,執(zhí)行該方法并調(diào)用 js 回調(diào)函數(shù)。
下面以安卓為例,通過(guò)覆蓋 WebChromeClient 類的 onJsPrompt 方法進(jìn)行攔截,android 端具體封裝會(huì)在下面單獨(dú)的板塊進(jìn)行說(shuō)明。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;
}
}
這種方式?jīng)]有太大缺點(diǎn),也不存在連續(xù)發(fā)送時(shí)信息丟失。不過(guò) iOS 的 UIWebView 不支持該方式(WKWebView 支持)。
2.3 注入 JS 上下文
即由 native 將實(shí)例對(duì)象通過(guò) webview 提供的方法注入到 js 全局上下文,js 可以通過(guò)調(diào)用 native 的實(shí)例方法來(lái)進(jìn)行通信。
具體有安卓 webview 的 addJavascriptInterface,iOS UIWebview 的 JSContext,iOS WKWebview 的 scriptMessageHandler。
下面以安卓 webview 的 addJavascriptInterface 為例進(jìn)行講解。
首先 native 端注入實(shí)例對(duì)象到 js 全局上下文,代碼大致如下,具體封裝會(huì)在下面的單獨(dú)板塊進(jìn)行講解: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 的方法轉(zhuǎn)換成 hashMap
JSBridge.register("JSBridge", NativeMethods.class);
// 將 JSBridge 的實(shí)例對(duì)象注入到 js 全局上下文中,名字為 _jsbridge,該實(shí)例對(duì)象下有 call 方法
mWebView.addJavascriptInterface(new JSBridge(mWebView), "_jsbridge");
}
}
public class NativeMethods {
// 用來(lái)供 js 調(diào)用的方法
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<>();
// 靜態(tài)方法,用于將傳入的第二個(gè)參數(shù)的類下面用于提供給 javacript 的接口轉(zhuǎn)成 Map,名字為第一個(gè)參數(shù)
public static void register(String exposeName, Class> classz) {
...
}
// 實(shí)例方法,用于提供給 js 統(tǒng)一調(diào)用的方法
@JavascriptInterface
public String call(String methodName, String args) {
...
}
}
然后 h5 端可以在 js 調(diào)用 window._jsbridge 實(shí)例下面的 call 方法,傳入的數(shù)據(jù)組合方式可以類似上面兩種方式。具體代碼如下: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 上下文的問(wèn)題
以安卓 webview 的 addJavascriptInterface 為例,在安卓 4.2 版本之前,js 可以利用 java 的反射 Reflection API,取得構(gòu)造該實(shí)例對(duì)象的類的內(nèi)部信息,并能直接操作該對(duì)象的內(nèi)部屬性及方法,這種方式會(huì)造成安全隱患,例如如果加載了外部網(wǎng)頁(yè),該網(wǎng)頁(yè)的惡意 js 腳本可以獲取手機(jī)的存儲(chǔ)卡上的信息。
在安卓 4.2 版本后,可以通過(guò)在提供給 js 調(diào)用的 java 方法前加裝飾器 @JavascriptInterface,來(lái)表明僅該方法可以被 js 調(diào)用。
上述三種 js 調(diào)用 native 的方式對(duì)比如下表:方式優(yōu)點(diǎn)缺點(diǎn)攔截 Url Schema(假請(qǐng)求)無(wú)安全漏洞1. 連續(xù)發(fā)送時(shí)消息丟失 2. Url 長(zhǎng)度限制,傳輸數(shù)據(jù)大小受限
攔截 prompt alert confirm無(wú)安全漏洞iOS 的 UIWebView 不支持該方式
注入 JS 上下文官方提供,方便簡(jiǎn)捷在安卓 4.2 以下有安全漏洞
3. 安卓端 java 的封裝
native 與 h5 交互部分的代碼在上面已經(jīng)提到了,這里主要是講述 native 端如何封裝暴露給 h5 的方法。
首先單獨(dú)封裝一個(gè)類 NativeMethods,將供 h5 調(diào)用的方法以公有且靜態(tài)方法的形式寫(xiě)入。如下:public class NativeMethods {
public static void showToast(WebView view, JSONObject arg, CallBack callBack) {
...
}
}
接下來(lái)考慮如何在 NativeMethods 和 h5 之前建立一個(gè)橋梁,JSBridge 類因運(yùn)而生。
JSBridge 類下主要有兩個(gè)靜態(tài)方法 register 和 call。其中 register 方法是用來(lái)將供 h5 調(diào)用的方法轉(zhuǎn)化成 Map 形式,以便查詢。而 call 方法主要是用接收 h5 端的調(diào)用,分解 h5 端傳來(lái)的參數(shù),查找并調(diào)用 Map 中的對(duì)應(yīng)的 Native 方法。
JSBridge 類的靜態(tài)方法 register
首先在 JSBridge 類下聲明一個(gè)靜態(tài)屬性 exposeMethods,數(shù)據(jù)類型為 HashMap 。然后聲明靜態(tài)方法 register,參數(shù)有字符串 exposeName 和類 classz,將 exposeName 和 classz 的所有靜態(tài)方法 組合成一個(gè) 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));
}
}
由上可知我們需要定義一個(gè) getAllMethod 方法用來(lái)將類里的方法轉(zhuǎn)化為 HashMap 數(shù)據(jù)格式。在該方法里同樣聲明一個(gè) HashMap,并將滿足條件的方法轉(zhuǎn)化成 Map,key 為方法名,value 為方法。
其中條件為 公有 public 靜態(tài) static 方法且第一個(gè)參數(shù)為 Webview 類的實(shí)例,第二個(gè)參數(shù)為 JSONObject 類的實(shí)例,第三個(gè)參數(shù)為 CallBack 類的實(shí)例。 (CallBack 是自定義的類,后面會(huì)講到)
代碼如下: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 類的靜態(tài)方法 call
由于注入 JS 上下文和兩外兩種,h5 端傳過(guò)來(lái)的參數(shù)形式不同,所以處理參數(shù)的方式略有不同。
下面以攔截 Prompt 的方式為例進(jìn)行講解,在該方式中 call 接收的第一個(gè)參數(shù)為 webView,第二個(gè)參數(shù)是 arg,即 h5 端傳過(guò)來(lái)的參數(shù)。還記得攔截 Prompt 方式時(shí) native 端和 h5 端約定的傳輸數(shù)據(jù)的方式么?"jsbridge://openScan?{"data": {"scanType": "qrCode"}, "cbName":"handleScanResult"}"
call 方法首先會(huì)判斷字符串是否以 jsbridge 開(kāi)頭(native 端和 h5 端之間約定的傳輸數(shù)據(jù)的協(xié)議名),然后該字符串轉(zhuǎn)成 Uri 格式,然后獲取其中的 host 名,即方法名,獲取 query,即方法參數(shù)和 js 回調(diào)函數(shù)名組合的對(duì)象。最后查找 exposeMethods 的映射,找到對(duì)應(yīng)的方法并執(zhí)行該方法。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 調(diào)用 native 方法成功后,native 有必要返回給 js 一些反饋,例如接口是否調(diào)用成功,或者 native 執(zhí)行后的得到的數(shù)據(jù)(例如掃碼)。所以 native 需要執(zhí)行 js 回調(diào)函數(shù)。
執(zhí)行 js 回調(diào)函數(shù)方式本質(zhì)是 native 調(diào)用 h5 的 js 方法,方式仍舊是上面提到的兩種方式 evaluateJavascript 和 loadUrl。簡(jiǎn)單來(lái)說(shuō)可以直接將 js 的回調(diào)函數(shù)名傳給對(duì)應(yīng)的 native 方法,native 執(zhí)行通過(guò) evaluateJavascript 調(diào)用。
但為了統(tǒng)一封裝調(diào)用回調(diào)的方式,我們可以定義一個(gè) CallBack 類,在其中定義一個(gè)名為 apply 的靜態(tài)方法,該方法直接調(diào)用 js 回調(diào)。
注意:native 執(zhí)行 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 執(zhí)行 js 方法時(shí),可接受 js 方法中異步返回的數(shù)據(jù),比如在 js 方法中請(qǐng)求某個(gè)接口在返回?cái)?shù)據(jù)。直接調(diào)用 webview 提供的 evaluateJavascript,在第二個(gè)參數(shù)的類 ValueCallback 的實(shí)例方法 onReceiveValue 并不能接收到 js 異步返回的數(shù)據(jù)。
后面有空 native 調(diào)用 js 方式會(huì)繼續(xù)完善的,最后以一句古語(yǔ)互勉:
路漫漫其修遠(yuǎn)兮 吾將上下而求索
總結(jié)
以上是生活随笔為你收集整理的jsbridge实现及原理_JSBridge 实现原理解析的全部?jī)?nèi)容,希望文章能夠幫你解決所遇到的問(wèn)題。
- 上一篇: 马斯克:SpaceX“星舰”发射推迟至周
- 下一篇: SpringBoot项目新手——问题疑惑