JsBridge交互框架的使用

                                   JsBridge交互框架的使用

现在很多App都采用了混合开发,对于展示性强的界面,可以用H5去实现;功能性强的的可以在用native实现。在混合开发中可以说native和JS进行交互肯定是要涉及到的,当然如果你们项目不是混合开发,某些地方只是需要展示一下H5界面即可,也就涉及不到这块。说到交互,虽然Android系统为我们提供的@JavascriptInterface这种交互方式,但是由于兼容性与安全性的问题,我们基本不再使用。今天所说的JsBridge就是安全性和兼容性比较好的一种交互方式,在混合式的开发中非常受欢迎。

     实现native层对JS的单向通信,只需要调用方法Webview.loadUrl("JavaScript:function()")即可。那如何实现H5层对native层的通信呢? JS层调用native层不是通过某一个方法就能直接调用的,他是一层一层调用的。为了方便理解,首先得提一下native层的WebChromClient这个类,这个类有三个方法分别是onJsAlert,onJsConfirm,onJsPrompt另外还得提一下JS层的window这个对象也有三个方法window.alertwindow.confirmwindow.prompt,当JS层调用windown对象中的某个方法的时候,相应的就会触发WebChromClient对象中对应的方法。说到这里你可能应该猜到了,我们就是通过这种响应机制来实现JS层对native层的通信。这三个方法用哪个呢?一般来说,我们是不会使用onJsAlert的,为什么呢?因为js中alert使用的频率还是非常高的,一旦我们占用了这个通道,alert的正常使用就会受到影响,而confirmprompt的使用频率相对alert来说,则更低一点。那么到底是选择confirm还是prompt呢,其实confirm的使用频率也是不低的,比如你点一个链接下载一个文件,这时候如果需要弹出一个提示进行确认,点击确认就会下载,点取消便不会下载,类似这种场景还是很多的,因此不能占用confirm。而prompt则不一样,在Android中,几乎不会使用到这个方法,就是用,也会进行自定义,所以我们完全可以使用这个方法。说到这,JS层对native层的通信的核心连接点已经说完了。你可能会疑惑,说好的JsBridge呢?毛都没看见,交互就结束了?千万别急啊,话说JSBridge就是一个协议,类似于http这种协议的东西。简单点说,这个协议就是native层和JS层约定好的一个协议,你JS层传给我的信息得按这个协议来进行包装,层层传递到native层。native层接收到这个信息的时候,也是根据这个协议进行解析获取我们需要的信息,比如你调我的哪个类,哪个方等等.这个后面会细说(jsbridge://className:port/methodName?jsonObj)

依赖:compile 'com.msj.javajsbridge:javajsbridge:1.0.4'

   上面说的,大家只要知道JS调用native层的调用机制就行了。JS调用下面直接上代码分析流程,每次只要按这个流程一步一步的走,实现起来也不是什么难事。

1.从JS层看起(下面的代码)。JS交互时候,JS层首先调用的是call(obj,method,params,callback)这个方法。(obj:调用的类名。method:调用哪个方法。params:参数。 callback:回调处理native返回给JS的处理结果。 port:随机生成的一个值,它是与callback对应的)。通过getUri将信息按协议封装成uri,在调用window.prompt(uri,“”)方法,这时候native层就会相应的调用onJsPrompt方法,并且会接收JS层传来的uri信息。onFinish(port,jsonObj)方法回调的时候用,暂时不说,只要注意下有这么个东西就行。
(function(win) {
    var hasOwnProperty = Object.prototype.hasOwnProperty;
    var JSBridge = win.JSBridge || (win.JSBridge = {});
    var JSBRIDGE_PROTOCOL = 'JSBridge';
    var Inner = {
        callbacks: {},
        call: function(obj, method, params, callback) {
            console.log(obj + " " + method + " " + params + " " + callback);
            var port = Util.getPort();
            console.log(port);
            this.callbacks[port] = callback;
            var uri = Util.getUri(obj, method, params, port);
            console.log(uri);
            window.prompt(uri, "");
        },
        onFinish: function(port, jsonObj) {
            var callback = this.callbacks[port];
            callback && callback(jsonObj);
            delete this.callbacks[port];
        },
    };
    var Util = {
        getPort: function() {
            return Math.floor(Math.random() * (1 << 30));
        },
        getUri: function(obj, method, params, port) {
            params = this.getParam(params);
            var uri = JSBRIDGE_PROTOCOL + '://' + obj + ':' + port + '/' + method + '?' + params;
            return uri;
        },
        getParam: function(obj) {
            if (obj && typeof obj === 'object') {
                return JSON.stringify(obj);
            } else {
                return obj || '';
            }
        }
    };
    for (var key in Inner) {
        if (!hasOwnProperty.call(JSBridge, key)) {
            JSBridge[key] = Inner[key];
        }
    }
})(window);

2.这里我们先自定义一个WebView类:主要用来进行一些设置。        第一,设置我们自定义的WebchromClient类。这里面为了扩展性,给自定义的WebchromClient加了个接口,方便在Activity或者Fragment中进行处理。     第二, register(“bridge”,CommonBridgeImp)反射获取native层CommonBridgeImp类中暴露给JS层的所有方法,并且存到一个HashMap<bridge,HashMap<方法名,Method>>集合中。

@Keep
public class CommonWebView extends WebView {

    /**
     * context of the webview page.
     */
    Context context;
    /**
     * the webview that show the page.
     */
    WebView webView;
    /**
     * activity of the webview.
     */
    Activity activity;
    /**
     * the root url of the website. url include index.html.
     */
    String rootUrl = "";

    /**
     * loading dialog.
     */
    LoadingDialog dialog;

    /**
     * ICommonActivityImp
     */
    ICommonActivityImp commonActivityImp;

    @Keep
    public CommonWebView(Context context,
                         String rootUrl,
                         ICommonActivityImp commonActivityImp) {
        super(context);
        this.context = context;
        this.rootUrl = rootUrl;
        this.activity = (Activity) context;
        this.commonActivityImp = commonActivityImp;

    }


    /**
     * init webview settings.
     */
    public void initWebView(final LoadingDialog dialog) {
        webView = this;
        this.dialog = dialog;
        WebSettings settings = webView.getSettings();
        settings.setJavaScriptEnabled(true);
        settings.setGeolocationEnabled(true);//webview华为7.0定位
        webView.setLayerType(View.LAYER_TYPE_SOFTWARE, null);//解决echats插件图表无法截图问题
        settings.setDomStorageEnabled(true);//设置允许本地存储
        settings.setAppCacheEnabled(true);
        settings.setAllowFileAccess(true);// 设置允许访问文件数据
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) {
            settings.setMixedContentMode(0);
        }
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) {
            settings.setMediaPlaybackRequiresUserGesture(true);
        }
        if (Build.VERSION.SDK_INT > Build.VERSION_CODES.ICE_CREAM_SANDWICH_MR1) {
            settings.setAllowFileAccessFromFileURLs(true);
            settings.setAllowUniversalAccessFromFileURLs(true);
        }
        if (Build.VERSION.SDK_INT < Build.VERSION_CODES.JELLY_BEAN_MR2) {
            webView.getSettings().setAppCacheMaxSize(1024 * 1024 * 8);
        }
        settings.setAppCachePath(context.getCacheDir().getAbsolutePath());
        settings.setCacheMode(WebSettings.LOAD_DEFAULT);
        CommonJSBridge.register("bridge", CommonBridgeImp.class);
        webView.setWebChromeClient(new CommonJSBridgeWebChromeClient(activity, commonActivityImp));
        webView.setWebViewClient(new WebViewClient() {

            @Override
            public void onPageStarted(WebView view, String url, Bitmap favicon) {
                super.onPageStarted(view, url, favicon);
                if (dialog != null && !dialog.isShowing()) {
                    dialog.show();
                }
            }

            @Override
            public boolean shouldOverrideUrlLoading(WebView view, String url) {
                view.loadUrl(url);
                return true;
            }

            @Override
            public void onPageFinished(WebView view, String url) {
                super.onPageFinished(view, url);
                if (dialog != null && dialog.isShowing()) {
                    dialog.dismiss();
                }
            }


            public void onReceivedSslError(WebView view, SslErrorHandler handler, SslError error) {
                handler.proceed();
            }
        });
       
    }

3.自定义WebChromClient这个类,通过onJsPrompt方法接收JS传来的uri,即是这里的message。onJsPrompt方法会调用result.confirm()==>CommonJsBridge.callJava();这里就是将uri传入到CommonJsBridge类中了,进行下一步的解析处理,并进行native层的方法调用。

public class CommonJSBridgeWebChromeClient extends WebChromeClient {
    Activity mActivity;
    ICommonActivityImp commonActivityImp;

    public CommonJSBridgeWebChromeClient(Activity activity, ICommonActivityImp commonActivityImp) {
       this. mActivity = activity;
       this. commonActivityImp=commonActivityImp;
    }

    /**
     * 核心方法,整个原生和h5交互的方式都是通过获取h5页面调用原生时通过osJsPrompt方法的传值进行交互
     * @param view
     * @param url
     * @param message (uri)
     * @param defaultValue
     * @param result
     * @return
     */
    @Override
    public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
        result.confirm(CommonJSBridge.callJava(view, message,commonActivityImp));
        return true;
    }

    @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 void onProgressChanged(WebView view, int newProgress) {
        mActivity.setProgress(newProgress);
        super.onProgressChanged(view, newProgress);
    }
}

我写的接口(仅供参考,根据自己业务需求定):

public interface ICommonImp{
}

public interface ICommonActivityImp extend ICommonImp {
    void jumpNextBanner(WebView webView, String type, String value, Callback callback);
}

   4.上面调用的callJava()方法,就在CommonJsBridge这个类里面,里面是根据JsBridge协议用来解析uri,可以看看下面的注释。获取将要调用的类名,方法名,以及Port  ,然后去集合中找到此类名的此方法。 一般的,我们将这些方法统一写到一个类中,通过反射获取这个类中的所有方法并保存到HashMap集合exposeMethods中。

public class CommonJSBridge {
    /**
     * 此方法通过WebView的onJsPrompt方法调用,为js调用原生的方法,根据方法名从methodMap拿到方法,反射调用,并将参数传进去
     *
     * @param webView
     * @param uriString 此参数为h5页面调用原生时的uri,格式如:(JSBridge://className:port/methodName?jsonObj),格式与JSBridge.js文件中定义一致
     * @return
     * @see CommonJSBridgeWebChromeClient# onJsPrompt(WebView, String, String, String, JsPromptResult)
     */
    public static String callJava(WebView webView, String uriString, ICommonImp baseWebImp) {
        String methodName = "";//暴露给js的类中的方法
        String className = "";//暴露给js的类
        String param = "{}";//json格式的参数,可是字符串,自己定义
        String port = "";//用于回调的端口号
        if (!TextUtils.isEmpty(uriString) && uriString.startsWith("JSBridge")) {
            Uri uri = Uri.parse(uriString);
            className = uri.getHost();
            param = uri.getQuery();
            port = uri.getPort() + "";
            String path = uri.getPath();
            if (!TextUtils.isEmpty(path)) {
                methodName = path.replace("/", "");
            }
        }

        if (exposedMethods.containsKey(className)) {
            HashMap<String, Method> methodHashMap = exposedMethods.get(className);

            if (methodHashMap != null && methodHashMap.size() != 0 && methodHashMap.containsKey(methodName)) {
                Method method = methodHashMap.get(methodName);
                if (method != null) {
                    try {
                        method.invoke(null, webView, param, new Callback(webView, port), baseWebImp);
                    } catch (Exception e) {
                        e.printStackTrace();
                    }
                }
            }
        }
        return null;
    }

    /**
     * 暴露的类方法集合<类,此类的所有方法>
     */
    private static Map<String, HashMap<String, Method>> exposedMethods = new HashMap<>();

    /**
     * 注册暴露给js页面的类
     *
     * @param
     */
    public static void register(String exposedName, Class<? extends IBridge> clazz) {
        if (!exposedMethods.containsKey(exposedName)) {
            try {
                exposedMethods.put(exposedName, getAllMethod(clazz));
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 反射获取暴露给js的类中的方法
     * 暴露的方法必须是public static ,参数必须是(WebView,String,Callback),此处可修改,与BridgeImpl类中暴露的方法一致
     *
     * @param injectedCls
     * @return
     * @throws Exception
     */
    private static HashMap<String, Method> getAllMethod(Class injectedCls) throws Exception {
        HashMap<String, Method> mMethodsMap = new HashMap<>();
        Method[] methods = injectedCls.getDeclaredMethods();
        for (Method method : methods) {
            String name;
            if (method.getModifiers() != (Modifier.PUBLIC | Modifier.STATIC) || (name = method.getName()) == null) {
                continue;
            }
            Class[] parameters = method.getParameterTypes();
            if (null != parameters && parameters.length == 4) {
                if (parameters[0] == WebView.class && parameters[1] == String.class && parameters[2] == Callback.class && parameters[3] == ICommonActivityImp.class) {
                    mMethodsMap.put(name, method);
                }
            }
        }
        return mMethodsMap;
    }
}

4.下面这个类CommonBridgeImp这个类是用来写暴露给JS层的所有的方法,注意方法必须用static  void 修饰,并且要实现空的接口IBridge主要是为了混淆的时候不会错。如果 需要Activity或者Fragment处理的调用上文自定义的ICommonBridgeImp这个接口回调。如果需要将数据返回给JS层或者再次调用JS层的方法,就通过Callback回调,将数据返回给JS层处理。

/**
 * 暴露给JS调用的原生的方法
 */
public class CommonBridgeImp implements IBridge {
    public static Handler handler = new Handler();
    public Context context;
    private CommonWebView commonWebView;

    private CommonBridgeImp(Context context, CommonWebView commonWebView) {
        this.context = context;
        this.commonWebView = commonWebView;
    }

    /**
     * 交互暴露的方法 fromJsFunction_test1
     * @param webView
     * @param param
     * @param callback
     */
    public static void fromJsFunction_test1(final WebView webView, String param, final Callback callback, final ICommonActivityImp commonActivityImp) {
        ArrayList<String> list = new ArrayList<>();
        list.add("type");
        list.add("value");
        String[] strArray = CommonUtil.getStrArray(param, list);
        final String type = strArray[0];
        final String value = strArray[1];
        handler.post(new Runnable() {
            @Override
            public void run() {
                commonActivityImp.jumpNextBanner(webView, type, value, callback);
            }
        });

    }

}

5.这个CallBack是个啥?为什么哪个里面都有它?返回给JS层为什么得经过他呢?这个得从JS层说起,简单理一下就明白了。JS层调用native层的时候,会先在JS层产生一个port值,然后构造一个Callback回调保存起来,并且将port值封装到Uri里面传给native层,native层会将这个port值封装到自定义Callback中。等到native处理完数据之后,再回调JS层的onFinish方法,并将参数和port值一起传递回去。JS层在回调会根据这个port值找到对应的Callback做进一步的处理,并将这个Callback删除。

package com.sgcc.cs.h5webviewpage.JSUtil;

import android.os.Handler;
import android.os.Looper;
import android.webkit.WebView;


import java.lang.ref.WeakReference;

/**********************************************************************************
 * @Version : V4.0
 * @Date: 2017/2/23 09:42
 * @Description: 作用 回调,原生应用处理完成后,回调js中回调方法的类
 * *********************************************************************************
 * History:   update past records <Author>  <Date>   <Version>
 * <p>
 * 修改内容:涉及文件:
 * *********************************************************************************
 */
public class Callback {
    private static Handler mHandler = new Handler(Looper.getMainLooper());
    /**
     * JSON串格式化参数
     */
    private static final String CALLBACK_JS_FORMAT_JSON = "javascript:JSBridge.onFinish('%s', %s);";
    /**
     * String串格式化参数,需要加引号,不然不会调用JSBridge.js中onFinish方法;
     * 原因:JavaScrip参数是String类型时,前后需要加引号
     */
    private static final String CALLBACK_JS_FORMAT_STRING = "javascript:JSBridge.onFinish('%s', \"%s\");";
    private String mPort;
    private WeakReference<WebView> mWebViewRef;

    public Callback(WebView view, String port) {
        mWebViewRef = new WeakReference<>(view);
        mPort = port;
    }

    /**
     * 若需要回调js中的回调方法,在相应的地方调用 Callback.apply(jsonObject)
     * @param jsonObject
     */
    public void apply(String jsonObject) {
        String resultStr;
        if (!"".equals(jsonObject)&&jsonObject.startsWith("{")){//判断是否为JSON,若不是,需要加引号
            resultStr = String.format(CALLBACK_JS_FORMAT_JSON, mPort, jsonObject);
        }else {
            resultStr = String.format(CALLBACK_JS_FORMAT_STRING, mPort, jsonObject);
        }
        final String execJs = resultStr;
        if (mWebViewRef != null && mWebViewRef.get() != null) {
            mHandler.post(new Runnable() {
                @Override
                public void run() {
                    mWebViewRef.get().loadUrl(execJs);
                }
            });
        }
    }
}

总结一下吧:

一:method.invoke(null, webView, param, new Callback(webView, port), baseWebImp);的使用场景:

1:如果你是打一个打的框架容器,用来加载H5页面的很多个业务模块可以把最后面的baseWebImp这个接口去掉,method.invoke(null, webView, param, new Callback(webView, port)。因为只需要接收JS层指令,并根据params中给的字段进行处理。如果需要将结果返回给Js层只需要调用callBack的apply即可。其实需要几个参数可以根据业务规范来定。个人还是喜欢按统一的规则来,舒心。

2:如果只是在某个页面,或者某几个页面中嵌入一个网页,回调需要在activity或者Fragment中进行处理。则method.invoke(null, webView, param, new Callback(webView, port), baseWebImp);这样更灵活一点。 

二:如果涉及到加载失败的默认图片这种情况,可以再WebViewChromClient  或者 WebViewClient 的错误方法中进行捕获并处理。

三:如果是第一种情况,作为容器还会涉及到H5模块的下载,解压,存储等,还有跳转,返回键 等这些琐碎的但是又是不可忽略的功能补充。

本人菜鸟一枚,如果有不好的,希望不吝指教,和谐交流,共同进步!

猜你喜欢

转载自blog.csdn.net/lk2021991/article/details/77318657