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

前言

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

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,793评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,567评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,342评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,825评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,814评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,680评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,033评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,687评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,175评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,668评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,775评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,419评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,020评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,206评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,092评论 2 351
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,510评论 2 343

推荐阅读更多精彩内容