前言
在Android上,對於JS互動,往往是通過系統原生提供的
@JavascriptInterface
這種方式進行互動的,而本人在專案的應該也是使用這種方式。最近聽朋友提到一個庫——JockeyJS,封裝了JS互動邏輯,通過少量的介面讓開發者只需要關注Java和JS之間的方法呼叫。我對它避開@JavascriptInterface
的實現比較感興趣,後來發現JockeyJS有於Java和JS之間的方法呼叫和回撥有著不錯的封裝,於是便有了分析JockeyJS一文。
一、JockeyJS基本使用
JockeyJS是幾年前的庫了,雖然是比較久的庫,但放到現在仍然可用。
首先,需要在h5頁面上引用專案中的jockey.js
接下來在客戶端進行配置,JockeyJS主要通過on(String type, JockeyHandler ... handler)
和send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
兩個方法來實現Java與JS之間的互動。
on(String type, JockeyHandler ... handler)
這一介面讓我們可以在Java上提供給JS需要呼叫的方法,類似於@JavascriptInterface
的功能,type
是我們提供的方法名,handler
中的回撥是我們執行的程式碼。
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呼叫JS方法,type
是呼叫的方法名,toWebView
是呼叫的webView,withPayload
是引數,會轉成json傳遞,complete
是呼叫成功後的回撥。
jockey.send("useJsFuntion", webView, null, new JockeyCallback() {
@Override
public void call() {
// secceed to use js function
}
});
複製程式碼
二、JockeyJS原理解析
參考JockeyJS提供的demo,在JockeyJS生效前,需要進行以下設定
jockey = JockeyImpl.getDefault();
jockey.configure(webView);
setJockeyEvents();
複製程式碼
JockeyImpl.getDefault()
這裡提供了對Jockey
介面的預設實現,也就是對於JS互動這一核心功能的實現。jockey.configure(webView)
向JockeyJS傳入webView,JockeyJS會對webView進行setJavaScriptEnabled(boolean)
和setWebViewClient(WebViewClient)
的設定。setJockeyEvents()
即一系列的on(String type, JockeyHandler ... handler)
操作,新增可供呼叫的Java方法。
這樣的話我們主要關注JockeyImpl.getDefault()
的實現。
public static Jockey getDefault() {
return new DefaultJockeyImpl();
}
複製程式碼
可見該方法返回的是DefaultJockeyImpl
。跟進DefaultJockeyImpl
,發現該類也是繼承了JockeyImpl
類的,我們先來看DefaultJockeyImpl
實現。主要看send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
。
1. Java呼叫JS的實現
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;
}
複製程式碼
該方法中有一個messageId
,這個messageId
是做什麼用的放在之後再解析。withPayload
這個容易理解,是用來傳遞引數的。接下來,webView進行loadUrl(String url)
,這個url是send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
方法的關鍵。url的格式是javascript:Jockey.trigger(\"%s\", %d, %s)
,即呼叫了Html的window.Jockey.trigger(type, messageId, json)
方法,JS會通過type去匹配相對應的函式並且呼叫,JS層的具體實現這裡不講。
在和JS互動的業務中,往往需要在呼叫完JS函式後有一個回撥,以便通知我們該函式執行完成,可以繼續後續操作。JockeyJS已經整合了這一邏輯。當呼叫send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
時,會將一個自增的messageId
和一個JockeyCallback
一一對應儲存在_callbacks
變數中,Java層將messageId
和函式名一起傳給JS,JS在執行完相關函式後,會使用該messageId
通知Java(通知方式見JS呼叫Java的實現
),Java層的JackeyJS通過messageId
找到JockeyCallback
並呼叫來完成回撥。這一層邏輯不暴露給開發者,開發者只需要關心JockeyCallback的實現,大大方便了回撥的處理。
2. JS呼叫Java的實現
JS呼叫Java不通過@JavascriptInterface
,那是怎麼呼叫的呢?通過JockeyImpl
類可以找到,JockeyJS對webView設定了自己的JockeyWebViewClient
,JockeyWebViewClient
的特別之處在於重寫了shouldOverrideUrlLoading(WebView view, String url)
方法。
public boolean shouldOverrideUrlLoading(WebView view, String url) {
...
if (isJockeyScheme(uri)) {
processUri(view, uri);
return true;
}
...
}
複製程式碼
這裡isJockeyScheme(uri)
對url進行了判斷:
public boolean isJockeyScheme(URI uri) {
return uri.getScheme().equals("jockey") && !uri.getQuery().equals("");
}
複製程式碼
當url的scheme為jockey
時,即url是以jockey://xxx這種格式存在時,JockeyJS會對該url進行攔截,交給應用自己處理,呼叫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"時,JockeyJS呼叫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
拿到對應的JockeyHandler
,這些JockeyHandler
就是我們初始化JockeyJS時通過on(String type, JockeyHandler ... handler)
加入的。接著perform(Map<Object, Object> payload, OnCompletedListener listener)
呼叫doPerform(Map<Object, Object> payload)
:
protected void doPerform(Map<Object, Object> payload) {
for (JockeyHandler handler : _handlers)
handler.perform(payload, this._accumulator);
}
複製程式碼
可以看到是對我們註冊的JockeyHandler
進行呼叫,這樣便實現了JS對Java方法的呼叫。
單單到這一步還沒完成JockeyJS的這一呼叫流程,接下來成JockeyJS會在doPerform(Map<Object, Object> payload)
完成後,通過triggerCallbackOnWebView(webView, messageId)
回撥JS,通知JS層方法已執行完畢,由JS去執行後續操作。
triggerCallbackOnWebView(webView, messageId)
的實現類似於send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
,在此就不贅述。
回到host的判斷,還有一種host為"callback"的情況,此時JockeyJS會呼叫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);
}
複製程式碼
很簡單,該方法是通知messageId從_callbacks中取出JockeyCallback並呼叫,即在上文中提到的send(String type, WebView toWebView, Object withPayload, JockeyCallback complete)
接收JS回撥的實現。
三、總結
JockeyJS無疑是封裝良好的用於JS互動的庫,不僅僅適用於Android,也相容iOS平臺。通過webView.loadUrl("javascript:xxx")
和shouldOverrideUrlLoading(WebView view, String url)
方法達到Java和JS的相互呼叫,並封裝了回撥邏輯,大大方便業務的開發。當然,隨著專案業務需求的增加,JockeyJS還是有可以優化的空間,但是JockeyJS的整體封裝值得參考,特別是對於初始專案,可以在JS互動上少走一點彎路。感興趣的同學也可以繼續閱讀JockeyJS在JS層和iOS層的程式碼實現。