JockeyJS——优秀的WebView与JS交互开源库使用和解析

1,917 阅读5分钟

前言

在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设置了自己的JockeyWebViewClientJockeyWebViewClient的特别之处在于重写了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层的代码实现。