前言
在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层的代码实现。