JockeyJS——優(yōu)秀的WebView與JS交互開源庫(kù)使用和解析

前言

在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)。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容