欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

混合开发框架整理:使用Crosswalk + WebViewJavascriptBridge进行混合开发

程序员文章站 2022-06-24 19:14:34
作者:hwj3747转载请注明Crosswalk介绍目前APP的开发模式大多基于H5+原生壳的开发模式,这时候使用到的WebView的性能就至关重要。我们知道,Android平台上,系统的碎片化比较严重,同Android版本的WebView的H5解析能力也有较大差异,导致相应的HTML5应用一致性难以保证。所以在做混合开发的时候,对Android系统的适配是一个比较麻烦的问题。这个时候,如果能在我们的APP嵌入一个第三方,不使用系统自带浏览器的话,这些问题就都迎刃而解了。Crosswalk就是这....

作者:hwj3747
转载请注明

Crosswalk介绍

目前APP的开发模式大多基于H5+原生壳的开发模式,这时候使用到的WebView的性能就至关重要。我们知道,Android平台上,系统的碎片化比较严重,同Android版本的WebView的H5解析能力也有较大差异,导致相应的HTML5应用一致性难以保证。所以在做混合开发的时候,对Android系统的适配是一个比较麻烦的问题。

这个时候,如果能在我们的APP嵌入一个第三方,不使用系统自带浏览器的话,这些问题就都迎刃而解了。Crosswalk就是这样一个第三方浏览器,其具有较好的H5性、功能支持,较好的平台一致性,以及近似原生应用的系统整合体验。

Crosswalk项目具有以下优势:

最大限度降低Android碎片化的影响,得到一致的,可预测的行为。
使用最新的Web技术及API。在Android 4.0+版本上提供丰富的功能。
使用Chrome DevTools轻松调试。(很方便的功能,可以直接在谷歌浏览器上进行调试,只不过需要*)
提升应用中HTML,CSS和JavaScript的性能。

WebViewJavascriptBridge介绍

WebViewJavascriptBridge 是盛名已久的 JSBridge 库,其主要作用是作为原生native与web JS沟通的“桥梁”,讲人话就是,能实现JS调用原生代码,以及原生调用JS的这么一个功能。目前该库已经累计获得1W+的start,并且已经有很多成熟的项目在使用这个库了,由此可见,该库是被广大开发者所认可的。

美中不足的是,目前官方只支持IOS版本的库。万幸它是开源的,许多人大神都基于它实现了支持Android版本的WebViewJavascriptBridge。比如说我使用到的是“大头鬼”开发的一个库:WebViewJavascriptBridge

准备工作

首先,先从crosswalk官网下载aar包
我用到的是这个包:crosswalk-23.53.589.4.aar
然后从github上down下WebViewJavascriptBridge库,导入Android studio
然后在该项目的基础上导入crosswalk的aar包:
集成方法:

  • 设置grade外部库为libs,拷贝aar文件到libs**
repositories {
    flatDir {
        dirs 'libs'
    }
}
  • 关联crosswalk库
compile(name: 'crosswalk-23.53.589.4', ext: 'aar')

然后同步一下代码就准备就绪了

使用Crosswalk和WebViewJavascriptBridge开发应用

打开项目,发现原本的项目用的还是系统自带的webview以及WebViewClient

public class BridgeWebView extends WebView implements WebViewJavascriptBridge 
public class BridgeWebViewClient extends WebViewClient

我们要做的就是把这两个替换成相应的crosswalk的XWalkView和XWalkResourceClient
所以新建一个类,模仿BridgeWebView 的写法,把BridgeWebView 相关的注册和处理handle,消息分发,等操作搬到自定义的XWalkView上。

@SuppressLint("SetJavaScriptEnabled")
public class MyXWalkView extends XWalkView implements WebViewJavascriptBridge {

    private final String TAG = "BridgeWebView";

    public static final String toLoadJs = "WebViewJavascriptBridge.js";
    Map<String, CallBackFunction> responseCallbacks = new HashMap<String, CallBackFunction>();
    Map<String, BridgeHandler> messageHandlers = new HashMap<String, BridgeHandler>();
    BridgeHandler defaultHandler = new DefaultHandler();

    private List<Message> startupMessage = new ArrayList<Message>();

    public List<Message> getStartupMessage() {
        return startupMessage;
    }

    public void setStartupMessage(List<Message> startupMessage) {
        this.startupMessage = startupMessage;
    }

    private long uniqueId = 0;

    public MyXWalkView(Context context, AttributeSet attrs) {
        super(context, attrs);
        init();
    }

    public MyXWalkView(Context context) {
        super(context);
        init();
    }

    /**
     *
     * @param handler
     *            default handler,handle messages send by js without assigned handler name,
     *            if js message has handler name, it will be handled by named handlers registered by native
     */
    public void setDefaultHandler(BridgeHandler handler) {
        this.defaultHandler = handler;
    }

    private void init() {
        this.setVerticalScrollBarEnabled(false);
        this.setHorizontalScrollBarEnabled(false);
        this.getSettings().setJavaScriptEnabled(true);
        if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) {
            WebView.setWebContentsDebuggingEnabled(true);
        }
        this.setResourceClient(new MyXWalkResourceClient(this));
    }

    /**
     * 获取到CallBackFunction data执行调用并且从数据集移除
     * @param url
     */
    void handlerReturnData(String url) {
        String functionName = BridgeUtil.getFunctionFromReturnUrl(url);
        CallBackFunction f = responseCallbacks.get(functionName);
        String data = BridgeUtil.getDataFromReturnUrl(url);
        if (f != null) {
            f.onCallBack(data);
            responseCallbacks.remove(functionName);
            return;
        }
    }

    @Override
    public void send(String data) {
        send(data, null);
    }

    @Override
    public void send(String data, CallBackFunction responseCallback) {
        doSend(null, data, responseCallback);
    }

    /**
     * 保存message到消息队列
     * @param handlerName handlerName
     * @param data data
     * @param responseCallback CallBackFunction
     */
    private void doSend(String handlerName, String data, CallBackFunction responseCallback) {
        Message m = new Message();
        if (!TextUtils.isEmpty(data)) {
            m.setData(data);
        }
        if (responseCallback != null) {
            String callbackStr = String.format(BridgeUtil.CALLBACK_ID_FORMAT, ++uniqueId + (BridgeUtil.UNDERLINE_STR + SystemClock.currentThreadTimeMillis()));
            responseCallbacks.put(callbackStr, responseCallback);
            m.setCallbackId(callbackStr);
        }
        if (!TextUtils.isEmpty(handlerName)) {
            m.setHandlerName(handlerName);
        }
        queueMessage(m);
    }

    /**
     * list<message> != null 添加到消息集合否则分发消息
     * @param m Message
     */
    private void queueMessage(Message m) {
        if (startupMessage != null) {
            startupMessage.add(m);
        } else {
            dispatchMessage(m);
        }
    }

    /**
     * 分发message 必须在主线程才分发成功
     * @param m Message
     */
    void dispatchMessage(Message m) {
        String messageJson = m.toJson();
        //escape special characters for json string  为json字符串转义特殊字符
        messageJson = messageJson.replaceAll("(\\\\)([^utrn])", "\\\\\\\\$1$2");
        messageJson = messageJson.replaceAll("(?<=[^\\\\])(\")", "\\\\\"");
        String javascriptCommand = String.format(BridgeUtil.JS_HANDLE_MESSAGE_FROM_JAVA, messageJson);
        // 必须要找主线程才会将数据传递出去 --- 划重点
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            this.loadUrl(javascriptCommand);
        }
    }

    /**
     * 刷新消息队列
     */
    void flushMessageQueue() {
        if (Thread.currentThread() == Looper.getMainLooper().getThread()) {
            loadUrl(BridgeUtil.JS_FETCH_QUEUE_FROM_JAVA, new CallBackFunction() {

                @Override
                public void onCallBack(String data) {
                    // deserializeMessage 反序列化消息
                    List<Message> list = null;
                    try {
                        list = Message.toArrayList(data);
                    } catch (Exception e) {
                        e.printStackTrace();
                        return;
                    }
                    if (list == null || list.size() == 0) {
                        return;
                    }
                    for (int i = 0; i < list.size(); i++) {
                        Message m = list.get(i);
                        String responseId = m.getResponseId();
                        // 是否是response  CallBackFunction
                        if (!TextUtils.isEmpty(responseId)) {
                            CallBackFunction function = responseCallbacks.get(responseId);
                            String responseData = m.getResponseData();
                            function.onCallBack(responseData);
                            responseCallbacks.remove(responseId);
                        } else {
                            CallBackFunction responseFunction = null;
                            // if had callbackId 如果有回调Id
                            final String callbackId = m.getCallbackId();
                            if (!TextUtils.isEmpty(callbackId)) {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        Message responseMsg = new Message();
                                        responseMsg.setResponseId(callbackId);
                                        responseMsg.setResponseData(data);
                                        queueMessage(responseMsg);
                                    }
                                };
                            } else {
                                responseFunction = new CallBackFunction() {
                                    @Override
                                    public void onCallBack(String data) {
                                        // do nothing
                                    }
                                };
                            }
                            // BridgeHandler执行
                            BridgeHandler handler;
                            if (!TextUtils.isEmpty(m.getHandlerName())) {
                                handler = messageHandlers.get(m.getHandlerName());
                            } else {
                                handler = defaultHandler;
                            }
                            if (handler != null){
                                handler.handler(m.getData(), responseFunction);
                            }
                        }
                    }
                }
            });
        }
    }


    public void loadUrl(String jsUrl, CallBackFunction returnCallback) {
        this.loadUrl(jsUrl);
        // 添加至 Map<String, CallBackFunction>
        responseCallbacks.put(BridgeUtil.parseFunctionName(jsUrl), returnCallback);
    }

    /**
     * register handler,so that javascript can call it
     * 注册处理程序,以便javascript调用它
     * @param handlerName handlerName
     * @param handler BridgeHandler
     */
    public void registerHandler(String handlerName, BridgeHandler handler) {
        if (handler != null) {
            // 添加至 Map<String, BridgeHandler>
            messageHandlers.put(handlerName, handler);
        }
    }

    /**
     * unregister handler
     *
     * @param handlerName
     */
    public void unregisterHandler(String handlerName) {
        if (handlerName != null) {
            messageHandlers.remove(handlerName);
        }
    }

    /**
     * call javascript registered handler
     * 调用javascript处理程序注册
     * @param handlerName handlerName
     * @param data data
     * @param callBack CallBackFunction
     */
    public void callHandler(String handlerName, String data, CallBackFunction callBack) {
        doSend(handlerName, data, callBack);
    }

接下来,同样新建一个类,模仿BridgeWebViewClient 的写法,拦截XWalkview的url,对消息进行处理,并且关键的是在加载完成的事件里,再加载一次WebViewJavascriptBridge.js这个文件,所以需要把WebViewJavascriptBridge.js这个文件拷贝到asset目录下。

public class MyXWalkResourceClient extends XWalkResourceClient {

    private MyXWalkView webView;

    public MyXWalkResourceClient(MyXWalkView view) {
        super(view);
        this.webView = view;
    }



    @Override
    public boolean shouldOverrideUrlLoading(XWalkView view, String url) {
        try {
            url = URLDecoder.decode(url, "UTF-8");
        } catch (UnsupportedEncodingException e) {
            e.printStackTrace();
        }

        if (url.startsWith(BridgeUtil.YY_RETURN_DATA)) { // 如果是返回数据
            webView.handlerReturnData(url);
            return true;
        } else if (url.startsWith(BridgeUtil.YY_OVERRIDE_SCHEMA)) { //
            webView.flushMessageQueue();
            return true;
        } else {
            return super.shouldOverrideUrlLoading(view, url);
        }
    }


    @Override
    public void onLoadStarted(XWalkView view, String url) {

        super.onLoadStarted(view, url);
    }

    @Override
    public void onLoadFinished(XWalkView view, String url) {
        super.onLoadFinished(view, url);

        if (BridgeWebView.toLoadJs != null) {
            webViewLoadLocalJs(view, MyXWalkView.toLoadJs);
        }

        //
        if (webView.getStartupMessage() != null) {
            for (Message m : webView.getStartupMessage()) {
                webView.dispatchMessage(m);
            }
            webView.setStartupMessage(null);
        }
    }

    /**
     * 这里只是加载lib包中assets中的 WebViewJavascriptBridge.js
     * @param view webview
     * @param path 路径
     */
    public static void webViewLoadLocalJs(XWalkView view, String path){
        String jsContent = assetFile2Str(view.getContext(), path);
        view.loadUrl("javascript:" + jsContent);
    }

    /**
     * 解析assets文件夹里面的代码,去除注释,取可执行的代码
     * @param c context
     * @param urlStr 路径
     * @return 可执行代码
     */
    public static String assetFile2Str(Context c, String urlStr){
        InputStream in = null;
        try{
            in = c.getAssets().open(urlStr);
            BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(in));
            String line = null;
            StringBuilder sb = new StringBuilder();
            do {
                line = bufferedReader.readLine();
                if (line != null && !line.matches("^\\s*\\/\\/.*")) { // 去除注释
                    sb.append(line);
                }
            } while (line != null);

            bufferedReader.close();
            in.close();

            return sb.toString();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            if(in != null) {
                try {
                    in.close();
                } catch (IOException e) {
                }
            }
        }
        return null;
    }
}

使用方法

接下来,看一下调用代码

  • web调用原生层:原生层使用registerHandler方法,注册一个js与原生交互的插件名称为“submitFromWeb”,js端只要调用callHandler方法并指定名称为“submitFromWeb”就能调用原生的代码,原生层在handler方法里面取得js传过来的值data,并且使用CallBackFunction 回调给原生层,大部分的场景都可以使用这个方法。
  • 原生层调用web,方法跟上面雷同,直接在原生层使用callHandler定义名称为“functionInJs”,在JS层注册一个名为functionInJs的handler就能接收到原生的消息了。

原生层:

webView.loadUrl("file:///android_asset/demo.html");

		webView.registerHandler("submitFromWeb", new BridgeHandler() {

			@Override
			public void handler(String data, CallBackFunction function) {
				Log.i(TAG, "handler = submitFromWeb, data from web = " + data);
                function.onCallBack("submitFromWeb exe, response data 中文 from Java");
			}

		});

        User user = new User();
        Location location = new Location();
        location.address = "SDU";
        user.location = location;
        user.name = "大头鬼";

        webView.callHandler("functionInJs", new Gson().toJson(user), new CallBackFunction() {
            @Override
            public void onCallBack(String data) {

            }
        });

        webView.send("hello");

h5层

 <script>
        function testDiv() {
            document.getElementById("show").innerHTML = document.getElementsByTagName("html")[0].innerHTML;
        }

        function testClick() {
            var str1 = document.getElementById("text1").value;
            var str2 = document.getElementById("text2").value;

            //send message to native
            var data = {id: 1, content: "这是一个图片 <img src=\"a.png\"/> test\r\nhahaha"};
            window.WebViewJavascriptBridge.send(
                data
                , function(responseData) {
                    document.getElementById("show").innerHTML = "repsonseData from java, data = " + responseData
                }
            );

        }

        function testClick1() {
            var str1 = document.getElementById("text1").value;
            var str2 = document.getElementById("text2").value;

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

        function bridgeLog(logContent) {
            document.getElementById("show").innerHTML = logContent;
        }

        function connectWebViewJavascriptBridge(callback) {
            if (window.WebViewJavascriptBridge) {
                callback(WebViewJavascriptBridge)
            } else {
                document.addEventListener(
                    'WebViewJavascriptBridgeReady'
                    , function() {
                        callback(WebViewJavascriptBridge)
                    },
                    false
                );
            }
        }

        connectWebViewJavascriptBridge(function(bridge) {
            bridge.init(function(message, responseCallback) {
                console.log('JS got a message', message);
                var data = {
                    'Javascript Responds': '测试中文!'
                };

                if (responseCallback) {
                    console.log('JS responding with', data);
                    responseCallback(data);
                }
            });

            bridge.registerHandler("functionInJs", function(data, responseCallback) {
                document.getElementById("show").innerHTML = ("data from Java: = " + data);
                if (responseCallback) {
                    var responseData = "Javascript Says Right back aka!";
                    responseCallback(responseData);
                }
            });
        })
    </script>

本文地址:https://blog.csdn.net/hwj3747/article/details/108572122