这也许是功能最强大的Android与Javascript交互开源库

项目中为了减少端上开发量,通常会使用一些跨平台的解决方案,而 web 就是最简单、兼容性最强的方案,但 web 又受制于浏览器,不能直接访问系统的一些属性,而且我们也需要 web 调用 native 的一些方法,所以我们需要一套 web 和 native 双向交互的方案。

目前,Android 要实现与 web 交互有以下几种常用方案:

  • WebView addJavascriptInterface方法
  • 拦截自定义协议链接实现数据交换
  • 实现 prompt,console等原生方法来数据交互

方案一是官方推荐实现方案,但是在 android 4.2以下存在严重安全漏洞,而且和 JS 交换的数据仅仅局限于基本类型(int,float,double,String 等),不支持直接 JS 函数调用和回调(需要通过注入 JS 支持), 案例:wendux/DSBridge-Android

方案二是兼容 iOS 的方案, 一般情况下前端需要依赖 JS 文件 或者 端上注入 JS, 调用方法固定,方法参数一般为: 函数名, 传递参数和回调函数, 传输数据长度就是 url 长度限制, 不支持同步回调,案例:lzyzsd / JsBridgemarcuswestin / WebViewJavascriptBridge

方案三是通过实现 Android WebView 原生方法来交互数据, 执行效率高,不限传输数据, 支持同步和异步传输,但也有弊端, 占据了系统函数,意味着前端使用这个函数就没效果了。

我们今天要介绍的库就是基于第三种方案的改进,基于 prompt 方法来实现 Android 与 Javascript 双向交互。 开源地址:https://github.com/pengwei1024/JsBridge

为什么说它功能强大呢?它可以实现你要想的任意 JS 方法,支持 JS 函数,对象,数组等所有基础类型的解析和回调。我们先来看个示例吧。

如果要实现一个分享功能要怎么做呢?

public class ServiceModule extends JsModule {  
    @Override
    public String getModuleName() {
        return "service";
    }

    @JSBridgeMethod
    public void share(String msg, final JBCallback success, final JBCallback failure) {
        Intent intent = new Intent(Intent.ACTION_SEND);
        intent.setType("text/plain");
        intent.putExtra(Intent.EXTRA_TEXT, msg);
        if (intent.resolveActivity(getContext().getPackageManager()) != null) {
            getContext().startActivity(Intent.createChooser(intent, "share"));
            success.apply("success");
        } else {
            failure.apply("failure");
        }
    }

JS 怎么调用呢?

JsBridge.service.share('分享内容',  
    function(){
        console.log('分享成功')
    }, 
    function(){
        console.log('分享失败')
    }
)

不用传递函数名, 调用的方法名和原生定义的方法名一致,参数支持所有 JS 类型(对象,数组等), 支持多个回调函数,回调函数参数也可以支持 JS 的所有类型,一看就和其他妖艳xx 不一样

你说 JS 可能没有成功失败的回调?没关系,我们支持缺省参数,下面这样也可以哟,不过原生方法 share 收到的 JBCallback 参数就为空咯

JsBridge.service.share('分享内容');  

数据还不够复杂?实际开发情况数据复杂多了。 那你看下面这样还行吗?JS 数组里基本包含了所有的类型

JsBridge.share.test(  
[ - 1111111111111111111, 1.235, 'hello world', true,
function(args) {  
    alert(args)
},
{
    a: 100101,
    b: function() {
        alert('执行复杂回调函数')
    }
},
[1, 2, 3, 4]]);

原生怎么解析这种情况呢?so easy, 不就一个 JBArray嘛

@JSBridgeMethod
public void test(JBArray array) {  
    for (int i = 0; i < array.size(); i++) {
            String output = "" + array.get(i);
            if (array.get(i) != null) {
                output += "##" + array.get(i).getClass();
            }
            Log.d(JsBridge.TAG, output);
     }
     array.getCallback(4).apply("xxx");
     array.getMap(5).getCallback("b").apply();
}

轻松回调 JS 数组里面的 function。JS 对象也不怕,JBMap全部搞定。

调用方法只能三个层级?我们项目不需要 module!!就想 JsBridge.test 直接调用。老铁当然没问题啦,只要继承JsStaticModule, 里面的方法分分钟变成静态,对象名直接调用。

public class ServiceModule extends JsStaticModule {  
    ...
}

我们项目要求多个层级呢?

需求还真多,多个层级是需要多少层级呢?4级够了吗?不行再多点?那你自己按需求定制吧!!

public class ServiceModule extends JsModule {  
    @Override
    public String getModuleName() {
        return "a.b.c.d.e.f.g";
    }
}    

现在调用路径如下, 我不敢保证 FE 看到这个方法会不会拿刀去找你

JsBridge.a.b.c.d.e.f.g.xx(...);  

iOS 要实现类似功能怎么办?

我们推荐使用 marcuswestin / WebViewJavascriptBridge,JsBridge 是可以兼容这个库的。怎么兼容呢?iOS 的 WebViewJavascriptBridge不主要就是一个方法嘛?

bridge.registerHandler("getCurrentPageUrl", function(data,responseCallback) {  
    responseCallback(document.location.toString())
})

一个静态方法实现足以 怎么都觉得配合 iOS 这么强大的库就算杀鸡用牛刀了!

介绍完功能,接下来我们从源码层面来介绍下JsBridge实现原理!

在分析之前,我们先来了解几个相关知识点

关键一: WebView 怎么执行 JS

在 API 19+, 可以用系统方法

 WebView.evaluateJavascript("alert(1)", null)

兼容所有版本的方案:

WebView.loadUrl("javascript:alert(1)")  

有一点需要明确,JS的执行是异步进行的

怎么利用执行 JS 注入一个对象呢?比如我需要一个对象JsBridge, 它包含一个 print()方法, JS 的语法是这样:

var JsBridge = {  
    print:function(msg){
        console.log(msg);
    }
}

WebView 只需要执行这段 JS 就好了

WebView.loadUrl("javascript:var JsBridge = {print:function(msg){console.log(msg);}")  

在 JS 中就可以直接用JsBridge.print(1)来调用这个方法了

关键二: onJsPrompt的参数了解

onJsPrompt 是 WebChromeClient 接口的一个回调方法,用来处理prompt方法的回调。

@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {  
     return true;
}

我们再看下 prompt的使用, 下面的代码网页中是什么效果呢?

prompt("输入你的名字", "张三");  

prompt 第一个参数是标题,对应onJsPrompt方法的 message, 第二个参数是需要输入的内容的默认值, 对应`onJsPrompt的defaultValue, 那JsPromptResult是用来干啥的呢?我们实现同步回调就完全靠它了,用来设置 prompt 方法的返回值,假如我们端上执行JsPromptResult.confirm("12"), 那么 prompt(xx) 的返回值就是12

基本的知识点我们都了解了,怎么串联起来上面的两条来实现WebView 和 JS 的双向交互呢?首先我们需要给 JS 提供调用方法, 如最早的JsBridge.service.share(...), 大概你已经知道了要用注入 JS 的方式。

WebView.loadUrl("javascript:alert(1)")  

这样,在 JS 就可以执行上面的方法,接下来的问题就是我们怎么把 JS 的数据传递给 Android 呢?prompt 要上场了!

JsBridge 这样的话,在onJsPrompt的回调里message 就取到了 title。你就要问了,success 和 error 怎么传递给 Android 呢?这两个类型是 function, Android 和 JS 的 变量并不能共享或者相互转换,所以是做不到把 JS 的变量传递到 Android 的!那怎么去解决这个问题呢?

既然不能转换,我们能不能换种思路,在分享执行完成的时候,我们需要执行 success 或者 error 来告诉 JS 分享结果,我们可以执行这个方法来实现呀,和 注入 JS 是一个套路,但是问题是对我们来说 success 和 error 是一个匿名方法(和 Java 的匿名内部类对象相似),我们并不知道怎么去调用它,这个容易解决呀,把这个函数赋值给一个已知名称的函数,然后我们在 Android 端调用已知名称的函数不就都解决了?我们来看下实现方式!

print()

Android 端实现

var JsBridge = {  
    print:function(msg){
        console.log(msg);
    }
}
这样 JS 就可以收到 success 回调了。

以上 Android 和 JS 的双向交互原理都讲明白了,我们来看下JsBridge这个库是怎么实现的!就是两步走,注入 JS 和 接受和处理 JS 参数并回调。

先看怎么注入的呢? JsBridge 需要继承JsModule来创建模块,然后模块里对方法添加JSBridgeMethod 注解的就是需要注入的方法。Java 方法的参数有一点的要求,和 JS 的参数有一个映射表

Java 类型 | 映射的 JS 类型
----|------ Boolean / boolean | Bool
Integer/ int | Number
Float / float | Number
Double / double | Number
Long / long | Number
String | String
JBCallback | function
JBMap | Object
JBArray | Array

然后通过JsBridgeConfig.getSetting().registerDefaultModule(NativeModule.class) 或者 JsBridge.loadModule(NativeModule.class) 来注册 module。

我们来看下注册的代码

WebView.loadUrl("javascript:var JsBridge = {print:function(msg){console.log(msg);}")  
将 module.class 通过反射实例化对象,并获取这个类里面的所有有效注册方法存在 Map<JsModule, Map<String, JsMethod>> 对象里面,注册就完成了,下面是注入 JS 的代码

JsBridge.print(1)Map<JsModule, Map<String, JsMethod>>对象依次取出 Module, 并划分静态 module 和非静态, 原理就是把需要的 JS 对象和方法注入进去,这里复杂的问题是多级调用层级的实现,如前面提到的JsBridge.a.b.c.d.e.f.g.xx(...);, 这也是注册 module 时为什么要对 module 进行排序的原因,这里需要有一定 JS 面向对象基础才能看得明白些,就不细讲了,有兴趣的去分析下吧。

再来就是获取 JS 的参数,并转化为 Java 的参数

onJsPrompt JsBridge 是将参数包装成JBArgumentParser对象,获取 module 名称 和 method 名称, 从之前保存的 Map<JsModule, Map<String, JsMethod>> 对象取出 JsMethod, JsMethod 里面包含了注解的 Java 反射的 Method 对象,通过反射调用 Method,并传递相应的参数 和 Context 对象,调用过程就完成了

怎么回调呢?回调方法被包装成了 JBCallback 类,其实里面就是包含了一个 Webview 和 已知回调函数名称,在需要回调的地方调用 WebView 执行回调方法。里面设计到了对象的转换,把 Java 类型转换成JS 类型

@Override
    public void apply(Object... args) {
        if (method == null || method.getModule() == null || method.getModule().mWebView == null
                || TextUtils.isEmpty(name)) {
            return;
        }
        String callback = method.getCallback();
        final StringBuilder builder = new StringBuilder("javascript:");
        builder.append("if(" + callback + " && " + callback + "['" + name + "']){");
        builder.append("var callback = " + callback + "['" + name + "'];");
        builder.append("if (typeof callback === 'function'){callback(");
        if (args != null && args.length > 0) {
            for (int i = 0; i < args.length; i++) {
                builder.append(JBUtils.toJsObject(args[i]));
                if (i != args.length - 1) {
                    builder.append(",");
                }
            }
        }
        builder.append(")}else{console.error(callback + ' is not a function')}}");
        mHandler.post(new Runnable() {
            @Override
            public void run() {
                if (method.getModule().mWebView instanceof WebView) {
                    ((WebView) method.getModule().mWebView).loadUrl(builder.toString());
                } else if (method.getModule().mWebView instanceof IWebView) {
                    ((IWebView) method.getModule().mWebView).loadUrl(builder.toString());
                }
            }
        });
    }

代码讲解就到这里,更多功能和介绍欢迎查看官方教程:https://github.com/pengwei1024/JsBridge