前言
在Android上,對(duì)于JS交互,往往是通過系統(tǒng)原生提供的
@JavascriptInterface這種方式進(jìn)行交互的,而本人在項(xiàng)目的應(yīng)該也是使用這種方式。最近聽朋友提到一個(gè)庫(kù)——JockeyJS,封裝了JS交互邏輯,通過少量的接口讓開發(fā)者只需要關(guān)注Java和JS之間的方法調(diào)用。我對(duì)它避開@JavascriptInterface的實(shí)現(xiàn)比較感興趣,后來發(fā)現(xiàn)JockeyJS有于Java和JS之間的方法調(diào)用和回調(diào)有著不錯(cuò)的封裝,于是便有了分析JockeyJS一文。
一、JockeyJS基本使用
JockeyJS是幾年前的庫(kù)了,雖然是比較久的庫(kù),但放到現(xiàn)在仍然可用。
首先,需要在h5頁(yè)面上引用項(xiàng)目中的jockey.js
接下來在客戶端進(jìn)行配置,JockeyJS主要通過on(String type, JockeyHandler ... handler)和send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
兩個(gè)方法來實(shí)現(xiàn)Java與JS之間的交互。
-
on(String type, JockeyHandler ... handler)這一接口讓我們可以在Java上提供給JS需要調(diào)用的方法,類似于@JavascriptInterface的功能,type是我們提供的方法名,handler中的回調(diào)是我們運(yùn)行的代碼。
jockey.on("useJavaMethod", new JockeyHandler() {
@Override
protected void doPerform(Map<Object, Object> payload) {
// do something
}
});
-
send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)用于Java調(diào)用JS方法,type是調(diào)用的方法名,toWebView是調(diào)用的webView,withPayload是參數(shù),會(huì)轉(zhuǎn)成json傳遞,complete是調(diào)用成功后的回調(diào)。
jockey.send("useJsFuntion", webView, null, new JockeyCallback() {
@Override
public void call() {
// secceed to use js function
}
});
二、JockeyJS原理解析
參考JockeyJS提供的demo,在JockeyJS生效前,需要進(jìn)行以下設(shè)置
jockey = JockeyImpl.getDefault();
jockey.configure(webView);
setJockeyEvents();
-
JockeyImpl.getDefault()這里提供了對(duì)Jockey接口的默認(rèn)實(shí)現(xiàn),也就是對(duì)于JS交互這一核心功能的實(shí)現(xiàn)。 -
jockey.configure(webView)向JockeyJS傳入webView,JockeyJS會(huì)對(duì)webView進(jìn)行setJavaScriptEnabled(boolean)和setWebViewClient(WebViewClient)的設(shè)置。 -
setJockeyEvents()即一系列的on(String type, JockeyHandler ... handler)操作,添加可供調(diào)用的Java方法。
這樣的話我們主要關(guān)注JockeyImpl.getDefault()的實(shí)現(xiàn)。
public static Jockey getDefault() {
return new DefaultJockeyImpl();
}
可見該方法返回的是DefaultJockeyImpl。跟進(jìn)DefaultJockeyImpl,發(fā)現(xiàn)該類也是繼承了JockeyImpl類的,我們先來看DefaultJockeyImpl實(shí)現(xiàn)。主要看send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)。
1. Java調(diào)用JS的實(shí)現(xiàn)
public void send(String type, WebView toWebView, Object withPayload, JockeyCallback complete) {
int messageId = messageCount;
if (complete != null) {
add(messageId, complete);
}
if (withPayload != null) {
withPayload = gson.toJson(withPayload);
}
String url = String.format("javascript:Jockey.trigger(\"%s\", %d, %s)",
type, messageId, withPayload);
toWebView.loadUrl(url);
++messageCount;
}
該方法中有一個(gè)messageId,這個(gè)messageId是做什么用的放在之后再解析。withPayload這個(gè)容易理解,是用來傳遞參數(shù)的。接下來,webView進(jìn)行loadUrl(String url),這個(gè)url是send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)方法的關(guān)鍵。url的格式是javascript:Jockey.trigger(\"%s\", %d, %s),即調(diào)用了Html的window.Jockey.trigger(type, messageId, json)方法,JS會(huì)通過type去匹配相對(duì)應(yīng)的函數(shù)并且調(diào)用,JS層的具體實(shí)現(xiàn)這里不講。
在和JS交互的業(yè)務(wù)中,往往需要在調(diào)用完JS函數(shù)后有一個(gè)回調(diào),以便通知我們?cè)摵瘮?shù)運(yùn)行完成,可以繼續(xù)后續(xù)操作。JockeyJS已經(jīng)集成了這一邏輯。當(dāng)調(diào)用send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)時(shí),會(huì)將一個(gè)自增的messageId和一個(gè)JockeyCallback一一對(duì)應(yīng)保存在_callbacks變量中,Java層將messageId和函數(shù)名一起傳給JS,JS在運(yùn)行完相關(guān)函數(shù)后,會(huì)使用該messageId通知Java(通知方式見JS調(diào)用Java的實(shí)現(xiàn)),Java層的JackeyJS通過messageId找到JockeyCallback并調(diào)用來完成回調(diào)。這一層邏輯不暴露給開發(fā)者,開發(fā)者只需要關(guān)心JockeyCallback的實(shí)現(xiàn),大大方便了回調(diào)的處理。
2. JS調(diào)用Java的實(shí)現(xiàn)
JS調(diào)用Java不通過@JavascriptInterface,那是怎么調(diào)用的呢?通過JockeyImpl類可以找到,JockeyJS對(duì)webView設(shè)置了自己的JockeyWebViewClient,JockeyWebViewClient的特別之處在于重寫了shouldOverrideUrlLoading(WebView view, String url)方法。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
...
if (isJockeyScheme(uri)) {
processUri(view, uri);
return true;
}
...
}
這里isJockeyScheme(uri)對(duì)url進(jìn)行了判斷:
public boolean isJockeyScheme(URI uri) {
return uri.getScheme().equals("jockey") && !uri.getQuery().equals("");
}
當(dāng)url的scheme為jockey時(shí),即url是以jockey://xxx這種格式存在時(shí),JockeyJS會(huì)對(duì)該url進(jìn)行攔截,交給應(yīng)用自己處理,調(diào)用processUri(WebView view, URI uri)。
public void processUri(WebView view, URI uri)
throws HostValidationException {
String[] parts = uri.getPath().replaceAll("^\\/", "").split("/");
String host = uri.getHost();
JockeyWebViewPayload payload = checkPayload(_gson.fromJson(
uri.getQuery(), JockeyWebViewPayload.class));
if (parts.length > 0) {
if (host.equals("event")) {
getImplementation().triggerEventFromWebView(view, payload);
} else if (host.equals("callback")) {
getImplementation().triggerCallbackForMessage(
Integer.parseInt(parts[0]));
}
}
}
JockeyJS從url中取出host和parts,判斷host為"event"時(shí),JockeyJS調(diào)用getImplementation().triggerEventFromWebView:
protected void triggerEventFromWebView(final WebView webView,
JockeyWebViewPayload envelope) {
final int messageId = envelope.id;
String type = envelope.type;
if (this.handles(type)) {
JockeyHandler handler = _listeners.get(type);
handler.perform(envelope.payload, new OnCompletedListener() {
@Override
public void onCompleted() {
_handler.post(new Runnable() {
@Override
public void run() {
triggerCallbackOnWebView(webView, messageId);
}
});
}
});
}
}
JockeyJS通過envelope.type從_listeners拿到對(duì)應(yīng)的JockeyHandler,這些JockeyHandler就是我們初始化JockeyJS時(shí)通過on(String type, JockeyHandler ... handler)加入的。接著perform(Map<Object, Object> payload, OnCompletedListener listener)調(diào)用doPerform(Map<Object, Object> payload):
protected void doPerform(Map<Object, Object> payload) {
for (JockeyHandler handler : _handlers)
handler.perform(payload, this._accumulator);
}
可以看到是對(duì)我們注冊(cè)的JockeyHandler進(jìn)行調(diào)用,這樣便實(shí)現(xiàn)了JS對(duì)Java方法的調(diào)用。
單單到這一步還沒完成JockeyJS的這一調(diào)用流程,接下來成JockeyJS會(huì)在doPerform(Map<Object, Object> payload)完成后,通過triggerCallbackOnWebView(webView, messageId)回調(diào)JS,通知JS層方法已執(zhí)行完畢,由JS去執(zhí)行后續(xù)操作。
triggerCallbackOnWebView(webView, messageId)的實(shí)現(xiàn)類似于send(String type, WebView toWebView, Object withPayload, JockeyCallback complete),在此就不贅述。
回到host的判斷,還有一種host為"callback"的情況,此時(shí)JockeyJS會(huì)調(diào)用getImplementation().triggerCallbackForMessage(int messageId):
protected void triggerCallbackForMessage(int messageId) {
try {
JockeyCallback complete = _callbacks.get(messageId, _DEFAULT);
complete.call();
} catch (Exception e) {
e.printStackTrace();
}
_callbacks.remove(messageId);
}
很簡(jiǎn)單,該方法是通知messageId從_callbacks中取出JockeyCallback并調(diào)用,即在上文中提到的send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)接收J(rèn)S回調(diào)的實(shí)現(xiàn)。
三、總結(jié)
JockeyJS無疑是封裝良好的用于JS交互的庫(kù),不僅僅適用于Android,也兼容iOS平臺(tái)。通過webView.loadUrl("javascript:xxx")和shouldOverrideUrlLoading(WebView view, String url)方法達(dá)到Java和JS的相互調(diào)用,并封裝了回調(diào)邏輯,大大方便業(yè)務(wù)的開發(fā)。當(dāng)然,隨著項(xiàng)目業(yè)務(wù)需求的增加,JockeyJS還是有可以優(yōu)化的空間,但是JockeyJS的整體封裝值得參考,特別是對(duì)于初始項(xiàng)目,可以在JS交互上少走一點(diǎn)彎路。感興趣的同學(xué)也可以繼續(xù)閱讀JockeyJS在JS層和iOS層的代碼實(shí)現(xiàn)。