JockeyJS——優秀的WebView與JS互動開源庫使用和解析

Windin發表於2018-06-15

前言

在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層的程式碼實現。

相關文章