JsBridge-总结和优化

前言

本文的介绍了WebView以及JsBridge的相关知识,所涉及到的Demo托管在gitlib上。

WebView

WebView是android中加载网页的一个控件,通过该控件可以加载网络/本地的HTML页面,同时也可以运行网络/本地的JS代码。

在WebView中,你可以通过特定方法进行JS和Native之间的相互调用,对于Native调用JS,有以下2种常用方法:

  • loadUrl方法
  • evaluateJavascript方法

而在页面中使用JS调用Native,在android的WebView中提供了以下常用的3种方法,相应的在JS中需要使用对应的调用函数:

  • addJavascriptInterface方法
  • WebViewClient中的shouldOverrideUrlLoading方法
  • WebChromeClient中的onJsxxx方法

Native调用JS方法

进行该操作的前提是需要在WebView中开启对JS的支持,需要在初始化WebView时添加如下代码:

// 设置与Js交互的权限
webSettings.setJavaScriptEnabled(true);

loadUrl

通过WebView中的loadUrl方法,在传入的参数中添加以“javascript:”为前缀,以对应的JS方法为后缀的字符串,使用该方法运行JS代码,在onPageFinished后才有效:

/**
 * 使用loadUrl来调用js
 */
private void callJavaScriptByLoad(){
    //参数名需要以javaScript为开头,方法名必须与js中对应。
    mWebView.loadUrl("javascript:callJS()");
}

evaluateJavascript

evaluateJavascript提供了一个性能更佳高效的选择,该方法不会去刷新页面,从而提高效率,且支持直接通过接口获取JS处理后的回调,但是只支持4.4+以上的系统。

/**
 * 使用evaluateJavascript方法,该方法需要API>19
 * 不会去刷新页面,因此效率更高
 */
@RequiresApi(api = Build.VERSION_CODES.KITKAT)
private void callJSByEvaluate(){
    // 只需要将第一种方法的loadUrl()换成下面该方法即可
    mWebView.evaluateJavascript("javascript:callJS()", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //此处为 js 返回的结果
            Toast.makeText(WebViewActivity.this, value, Toast.LENGTH_SHORT).show();
        }
    });
}

以上均会调用JS中名为callJS的函数。

JS调用Native方法

addJavascriptInterface

addJavascriptInterface是WebView提供的一个可以让JS直接访问Native的方法。

1.声明一个提供JS调用的类,该类提供JS使用的方法

public class JSCallJava extends Object{

    private Context mContext;

    public JSCallJava(Context context){
        mContext = context;
    }

    @JavascriptInterface
    public String callJava(){
        Toast.makeText(mContext, "hello java", Toast.LENGTH_SHORT).show();
        return "fromJavascriptInterface";
    }

}

2.在addJavascriptInterface中传入需要提供给JS使用的类,以及对应的在JS中的对象名

    /**
     * 第1种:
     * 映射的JS对象名
     * 4.2版本前直接使用该方法存在严重漏洞
     */
    @SuppressLint("JavascriptInterface")
    private void forJSByJSInterface() {
        //@param o Java方法的对象
        //@param jsMethodName Javascript对象名
        mWebView.addJavascriptInterface(new JSCallJava(this), "caller");
    }

3.在JS代码中调用对应native方法:

function testClick1() {
    //call native method
    var result = caller.callJava();
    document.getElementById("text3").value = result;

}

需要注意的是,在早期版本中(4.2版本前),该方式存在严重的漏洞,攻击者可以通过JS加载java中的反射类去执行本地命令,虽然在高版本中通过使用@JavascriptInterface进行规避该漏洞,但是老版本的问题依旧存在。

WebViewClient的shouldOverrideUrlLoading

WebView中,可以通过拦截URL跳转来实现JS调用Native方法,通过自定义uri协议格式来传递值,在WebViewClient的shouldOverrideUrlLoading中进行拦截处理。

1.为WebView设置WebViewClient,拦截URL跳转:

mWebView.setWebViewClient(new WebViewClient(){

    @Override
    public boolean shouldOverrideUrlLoading(WebView view, String url) {
        Uri uri = Uri.parse(url);
        if ("js".equals(uri.getScheme())) {
            if ("webview".equals(uri.getAuthority())) {
                Toast.makeText(WebViewActivity.this, "shouldOverrideUrlLoading", Toast.LENGTH_SHORT).show();
            }
            //不进行跳转
            return true;
        }
        return super.shouldOverrideUrlLoading(view, url);
    }
});

2.在JS中通过设置URL来进行调用

    function testClick() {
            /*约定的url协议为:js://webview?arg1=111&arg2=222*/
            document.location = "js://webview?arg1=111&arg2=222";
        }

WebChromeClient的onJsxxx

通过设置WebChromeClient,拦截JS的对话框来实现对Native的调用。

在WebChromeClient存在三种弹框,分别为alert,confirm和prompt。三者除了弹窗级别不一样,还有返回的参数不同,其中prompt相对其他两个在JS使用率低,同时可以返回任意参数,所以prompt是最合适用与调用Native的入口。

1.设置WebChromeClient,重写onJsPrompt方法

        mWebView.setWebChromeClient(new WebChromeClient(){
            @Override
            public boolean onJsAlert(WebView view, String url, String message, JsResult result) {
                return super.onJsAlert(view, url, message, result);
            }

            @Override
            public boolean onJsConfirm(WebView view, String url, String message, JsResult result) {
                return super.onJsConfirm(view, url, message, result);
            }

            @Override
            public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
                // 根据协议的参数,判断是否是所需要的url(原理同方式2)
                // 一般根据scheme(协议格式) & authority(协议名)判断(前两个参数)
                //假定传入进来的 url = "js://webview?arg1=111&arg2=222"(同时也是约定好的需要拦截的)

                Uri uri = Uri.parse(message);
                // 如果url的协议 = 预先约定的 js 协议
                // 就解析往下解析参数
                if ( uri.getScheme().equals("js")) {

                    // 如果 authority  = 预先约定协议里的 webview,即代表都符合约定的协议
                    // 所以拦截url,下面JS开始调用Android需要的方法
                    if (uri.getAuthority().equals("demo")) {

                        //
                        // 执行JS所需要调用的逻辑
                        Toast.makeText(WebViewActivity.this, "js调用了Android的方法", Toast.LENGTH_SHORT).show();
                        // 可以在协议上带有参数并传递到Android上
                        HashMap params = new HashMap<>();
                        Set collection = uri.getQueryParameterNames();

                        //参数result:代表消息框的返回值(输入值)
                        result.confirm("fromPrompt");
                    }
                    return true;
                }
                return super.onJsPrompt(view, url, message, defaultValue, result);
            }
        });

2.JS中使用弹窗进行回调:

function testClick2() {
    //call native method
    // 调用prompt()
    var result=prompt("js://demo?arg1=111&arg2=222");
    document.getElementById("text3").value = result;

}

JsBridge

JsBridge是一个Native和Html交互的一个桥梁,类似RPC的一个控件,使用该控件可以方便的实现JS和Native之间的相互调用和数据的传递。

原理简介

在JsBridge中,最重要的是要制定如下两个协议:

  • 数据的序列化协议
  • 调用控制协议

前者使用了json和URL,而后者,在java层中,以下三个接口中可以查看:

JsBridge-总结和优化_第1张图片
alt

对于协议的实现,对应在Native和JS的桥接类:

  • BridgeWebView
  • WebViewJavascriptBridge.js
JsBridge-总结和优化_第2张图片
alt

在WebView中提到了2中调用JS的方法以及3种调用Java的方法,而在JsBridge中,其实就是使用了loadUrl和shouldOverrideUrlLoading,来实现JS和Native的互调。

在实际使用中,需要用BridgeWebView替代WebView,该类在init时,会默认去加载一个BridgeWebViewClient,而Client重写了shouldOverrideUrlLoading来对从JS获取到的数据进行分发,另外,也在onPageFinished中将WebViewJavascriptBridge.js加载到html页面中。

使用

在Android Studio中使用JsBridge,需要在project的build.gradle中添加maven库:

maven { url "https://jitpack.io" }

然后再module中添加如下依赖:

dependencies {
    compile 'com.github.lzyzsd:jsbridge:1.0.4'
}

在使用前,可以分别在Native和HTML中设置默认的handle,以处理未指定handle的调用。

Native中:

mBridgeWebView.setDefaultHandler(new DefaultHandler());

HTML中:

bridge.init(function(message, responseCallback) {
    …
    responseCallback(data);
});

Native调用JS方法

JsBridge通过实现一个自定义的WebView——BridgeWebView以及在JS层提供WebViewJavascriptBridge.js来实现两端使用代码的简化和统一。所有的操作最后都会在handle中处理,

在Native中,最简单的使用,通过send方法调用JS

mBridgeWebView.send(data);

该方法还可以设置一个回调,接收从JS获取到的结果:

mBridgeWebView.send(data, new CallBackFunction() {
    @Override
    public void onCallBack(String data) {

    }
});

send方法会使用JS中默认的handle进行数据的处理。

更多时候,我们需要使用自定义的方法,这时我们需要在Native中使用callHandler来调用JS:

mBridgeWebView.callHandler(handlerName, data, new CallBackFunction() {
    @Override
    public void onCallBack(String data) {
        …
    }
});

该方法可以指定JS中的handle来处理该数据,当然你需要在JS中注册对应的handle:

bridge.registerHandler(handlerName, function(data, responseCallback) {
    …
    responseCallback(responseData);
});

这样就完成了从Native到JS的一次调用。

JS调用Native方法

因为JsBridge在WebViewJavascriptBridge.js的封装,我们可以在JS中以类似的步骤实现对Native层调用。

使用send方法调用Java中默认的handle:

window.WebViewJavascriptBridge.send(
                data
                , function(responseData) {
                    …
                }
            );

使用callHandle调用Native中指定的方法处理:

window.WebViewJavascriptBridge.callHandler(
    'submitFromWeb'
    , {'param': '中文测试'}
    , function(responseData) {
        document.getElementById("show").innerHTML = "send get responseData from java, data = " + responseData
    }
);

相应的,你需要在Native中注册好对应的handle来处理事件:

mBridgeWebView.registerHandler(handleName, new BridgeHandler() {
    @Override
    public void handler(String data, CallBackFunction function) {
        …
    }
});

以上就可完成一次JS到Native的调用。

XGBridge

XGBridge是参考了一起赚/一起逛项目中的Bridge的实现,以及对JsBridge的二次封装。Demo托管在gitlib上的jsbridgedemo。大致的结构如下图所示:

JsBridge-总结和优化_第3张图片
alt

XGBridge封装在baseSDK中,而Native和JS之间互相调用方法需要依赖一些本地视图以及业务相关,所以抽离出来到project中实现,对应的JavaApi封装了JS使用的Native代码,JSApi则提供了可调用的JS方法。

XGBridge通过抽离WebView降低耦合度,其实例化依赖于WebView,因此,在含有WebView的界面中,通过如下构造方法完成实例化即可:

mXGBridge = new XGBridge(mWebView);

Api的实现,依赖于XGBridge中提供的以下两个方法实现通信:

  • callHandler
  • registerXGBridgeHandler

而具体的处理方法,需要和前端一起制定统一的调用协议,同时可以根据需求不断添加,完善。

Native调用JS方法

所有JS方法都将在JSApi中封装,其通过XGBridge中设置需要调用的JS方法名,传入参数以及回答借口实现。比如一个getJSContent来获取H5页面中的特定数据,JSApi中实现如下(需要对应在JS中实现getJSContent方法):

public class JSApi{
    …
    /**
    * 获取H5的信息
    * @param callback JS处理后的回调,包括其处理后的返回参数
    */
    public void getJSContent(CallBackFunction callback){
        mXGBridge.callHandler("getJSContent", null, callback);
    }

}

在需要使用的Activity中,实例化JSApi,然后在需要的事件中调用JS方法,Demo中展示了一个按钮触发该事件的一个过程:

//实例化JSApi
mJSApi = new DemoJSApi(mXGBridge);

…

//通过按钮触发,调用JS代码
mButton.setOnClickListener(new View.OnClickListener() {
    @Override
    public void onClick(View v) {
        //调用JS代码
        mJSApi.getJSContent(new CallBackFunction() {
            @Override
            public void onCallBack(String data) {
                Toast.makeText(H5Activity.this, data, Toast.LENGTH_SHORT).show();
            }
        });
    }
});

JS调用Native方法

JavaApi提供了给JS调用的handler,通过注册这些方法,实现JS端对Native端端调用。
编写一个Api,需要实现IXGBridgeHandler接口,该接口需要传入一个方法名(对应JS中调用的方法名),以及一个handler来执行相应的处理,比如提供JS关于Android系统的版本号等信息的IXGBridgeHandler:

public static IXGBridgeHandler getNativeInfo(final Context context){
    return new IXGBridgeHandler() {
        @Override
        public String getMethodName() {
            return "getNativeInfo";
        }

        @Override
        public void handler(String data, CallBackFunction function) {
            Toast.makeText(context, data, Toast.LENGTH_SHORT).show();
            function.onCallBack(android.os.Build.VERSION.RELEASE);
        }
    };
}

然后在需要使用的Activity中进行注册:

//注册Java的handler,用于JS调用
mXGBridge.registerXGBridgeHandler(JavaApi.getNativeInfo(this));
mXGBridge.registerXGBridgeHandler(JavaApi.setTitle(this));

总结

根据以上整理总结,对于Native调用JS的几个方法对比如下:

方法 发送数据 回调接口 兼容性
loadUrl
evaluateJavascript 仅4.4以上支持
JsBridge

JS调用Native的对比:

方法 发送数据 回调接口 安全性
addJavascriptInterface 4.2以下存在严重的漏洞
shouldOverrideUrlLoading
onJsPrompt
JsBridge

相关参考

  • JsBridge 实现 JavaScript 和 Java 的互相调用
  • 你要的WebView与 JS 交互方式 都在这里了

你可能感兴趣的:(JsBridge-总结和优化)