JSBridge总结

作者: NowhereToRun | 来源:发表于2019-02-18 00:35 被阅读29次

    由于Webview内嵌H5的性能/功能各种受限,于是有了各种的混合开发解决方案,例如Hybrid、RN、WEEX、Flutter、小程序、快应用等等。

    React Native 至今没有推出1.0版本,由于各种可能的坑,一些hold不住的团队可能会放弃。
    Flutter 是否可替代RN,真正实现两端统一,拭目以待,他从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。我本人之后会关注学习一下。
    小程序 不用说太多了,大家都很熟悉了;微信、支付宝、百度都在用。除了第一次需要花点时间下载,体验上可以说是很不错了,但是封闭性是他很大的一个缺点。
    快应用 目标是很好的,统一API,但是还是要看各厂家的执行力度。

    现在来总结一下我们团队目前使用的Hybrid方案。算是回顾一下,巩固基础,好记性不如烂笔头。

    一、Hybrid简介

    Hybrid可以说是上面提到的几种里最古老,最成熟的解决方案了。

    缺点是明显的:H5有的缺点他几乎都有,比如性能差、JS执行效率低等等。

    但是优点也很显著:随时发版,不受应用市场审核限制(当然这个前提是Hybrid对应Native的功能都已准备就绪);拥有几乎和Native一样的能力,eg:拍照、存储、加日历等等...

    基本原理

    Hybrid利用JSBridge进行通信的基本原理网上一搜一大把,简单记录一下。

    Native => JS
    两端都有现成方法。谁让都在别人的地盘下面玩呢,Native当然有办法来执行JS方法。
    iOS

    // Swift
    webview.stringByEvaluatingJavaScriptFromString("Math.random()")
    // OC
    [webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];
    

    Android

    mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
            @Override
            public void onReceiveValue(String value) {
                //这里的value即为对应JS方法的返回值
            }
    });
    

    JS => Native
    对于Webview中发起的网络请求,Native都有能力去捕获/截取/干预。所以JSBridge的核心就是设计一套url方案,让Native可以识别,从而做出响应,执行对应的操作就完事。
    例如,正常的网络请求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
    我们可以自定义协议,改成jsbridge://methodName?param1=value1&param2=value2
    Native拦截jsbridge开头的网络请求,做出对应的动作。
    最常见的做法就是创建一个隐藏的iframe来实现通信。

    二、现成的解决方案

    iOS WebViewJavascriptBridge
    Android JsBridge

    基本原理都相同,项目的设计就决定了一个它的可扩展性&可维护性。良好的可扩展性&可维护性对于JSBridge尤为重要,他是后面一切业务的基石。

    基础库简析

    (下面都以Android为例)

    1、 初始化

    类似写普通H5页面需要监听DOMContentLoaded或者onLoad来决定开始执行脚本一样,JSBridge需要一个契机去告诉JS,我准备好了,你可以来调用我的方法了。

    [前端] 执行监听 && 检测

        if (window.WebViewJavascriptBridge) {
            //do your work here
        } else {
            document.addEventListener(
                'WebViewJavascriptBridgeReady'
                , function() {
                    //do your work here
                },
                false
            );
        }
    

    [Native (埋在端里的JS)] dispatchEvent触发

        var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
            init: init,
            send: send,
            registerHandler: registerHandler,
            callHandler: callHandler,
            _fetchQueue: _fetchQueue,
            _handleMessageFromNative: _handleMessageFromNative
        };
        
        var readyEvent = doc.createEvent('Events');
        readyEvent.initEvent('WebViewJavascriptBridgeReady');
        readyEvent.bridge = WebViewJavascriptBridge;
        doc.dispatchEvent(readyEvent);
    
    2、JS调Native方法

    先上代码,下面是埋在端内的,JSBridge.callHandler,用来实现JS调用Native。

        // 调用线程
        function callHandler(handlerName, data, responseCallback) {
            _doSend({
                handlerName: handlerName,
                data: data
            }, responseCallback);
        }
    
        //sendMessage add message, 触发native处理 sendMessage
        function _doSend(message, responseCallback) {
            if (responseCallback) {
                var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
                responseCallbacks[callbackId] = responseCallback;
                message.callbackId = callbackId;
            }
    
            sendMessageQueue.push(message);
            messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
        }
    

    jsbridge.callHandler是JS调Native方法的核心。
    handlerName是前端与Native协商好的方法名称
    data 参数
    responseCallback 回调
    回调函数绑在了一个内部对象中var responseCallbacks = {},发送给Native的消息message中只包含了这个回调函数对应的id,端上处理完成之后触发&销毁。

    这个方法并不直接把消息全部推送走,而是存在一个队列中sendMessageQueue。同时通知Native,有新数据(message)需要处理。即上面代码的最后一行,他利用iframe的src通知端上的信息如下:

        var CUSTOM_PROTOCOL_SCHEME = 'sn'
        var QUEUE_HAS_MESSAGE = '__sn__queue_message__'
    

    上面提到的,JS只是通知了端上有新消息,Native调用获取时机暂时不考虑,就假设他收到一条就处理一次,极端高频情况下,两三条处理一次。Native通过_fetchQueue统一处理存储在sendMessageQueue中的数据:

        // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
        function _fetchQueue() {
            var messageQueueString = JSON.stringify(sendMessageQueue);
            sendMessageQueue = [];
            //android can't read directly the return data, so we can reload iframe src to communicate with java
            if (messageQueueString !== '[]') {
                bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
            }
        }
    

    这些基本就是JS主动调用Native的流程,关于回调方法,下面统一说。

    3、Native调JS方法

    虽说Native可以随意执行JS,但是总是需要知道哪些JS方法是可执行的吧。registerHandler就是用来执行注册。
    registerHandler在Native端定义(是JSBridge对象的一个方法),由前端来注册。

        // 注册线程 往数组里面添加值
        function registerHandler(handlerName, handler) {
            messageHandlers[handlerName] = handler;
        }
    

    Native主动调用。
    Native主动调用分两种情况,1是Native主动触发前端事件,例如通知前端页面可视状态变化。2是前端调用Native的回调。JSBridge是天生异步的,所以回调和主动调用归结到一类里面了。
    如果是前端主动调用的方法,有responseId,即有回调,直接调用执行即可。
    否则就去注册的messageHandlers中寻找方法,调用。

        //提供给native使用,
        function _dispatchMessageFromNative(messageJSON) {
            setTimeout(function() {
                var message = JSON.parse(messageJSON);
                var responseCallback;
                //java call finished, now need to call js callback function
                // 前端主动调用的Callback
                if (message.responseId) {
                    responseCallback = responseCallbacks[message.responseId];
                    if (!responseCallback) {
                        return;
                    }
                    responseCallback(message.responseData);
                    delete responseCallbacks[message.responseId];
                } else {
                    // Native主动调用
                    //直接发送
                    if (message.callbackId) {
                        var callbackResponseId = message.callbackId;
                        responseCallback = function(responseData) {
                            _doSend({
                                responseId: callbackResponseId,
                                responseData: responseData
                            });
                        };
                    }
    
                    var handler = WebViewJavascriptBridge._messageHandler;
                    if (message.handlerName) {
                        handler = messageHandlers[message.handlerName];
                    }
                    //查找指定handler
                    try {
                        handler(message.data, responseCallback);
                    } catch (exception) {
                        if (typeof console != 'undefined') {
                            console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                        }
                    }
                }
            });
        }
    

    代码分析基本就到这里,盗一张图(地址放在最后了),把流程都画了出来,个人感觉没啥问题

    三、业务封装

    直接使用前面的库可以完成功能,但是不够优雅,代码不经过良好的设计可能会变得牵一发动全身,可维护性差。下面说说我们的设计,可能不是最好的,但是是很符合我们业务场景的。

    1. 事件基础类 EventClass
      处理事件广播、订阅。
    2. 连接基础类 ConnectClass
    /**
     * 创建和获取 jsbridge 基础类
     * @class ConnectClass
     * @extends EventsClass
     */
    class ConnectClass extends EventsClass {
    
      /**
       * 获取jsbridge实例,注入到sncClass上的bridge属性 `this.bridge`
       */
      connect() {
         // 事件广播,通知开始建立连接,统计使用
         // 建立JSBridge
         // 建立JSBridge.then  1.注册Native主动调用的事件,对应上面的bridge.registerHandler;2.广播 建立完成,统计使用
      }
    
      // ... 其他的一些方法
     // eg: 分平台初始化JSBridge,处理差异性
     // eg: bridge.registerHandler 回调的封装一层的统一处理函数
    
    }
    

    关于注册Native主动调用的事件(和下面会提到的JS主动调用事件),实现插件化,并同一封装。好处是可以明确代码执行步骤、方便业务同学调试(这不是我的锅,我已经执行调用了...)、方便性能统计。

    1. 业务类
    class SncClass extends ConnectClass {
      constructor(option){
        // 监听connect,监听首屏数据
        // 建立连接 this.connect
        // 挂载必备API
      }
    
      // 初始化,根据参数决定挂载哪些api
      init(apis){
        this.mountApi(apis);
      }
    
      /**
       * 挂载 api
       * @param  {Object} apis api 对象集合
       */
      mountApi(apis) {
        // 1.  错误处理
        // 2. 检测是否已经jsb建立连接 已连接则 直接执行真正挂载函数 return
        // 3. bridge 未初始化时,定义方法预声明。执行的方法将会被储存在缓存队列里在 bridge 初始化后调用
        // 4. 监听连接事件,执行真正挂载 loadMethods
      }
    }
    
     /**
       * 加载 API 到实例属性,标志着 api 的真正挂载
       */
      loadMethods(apis) {
          // 1.  防止重复挂载 api,
          // 2. 给插件初始化方法注入ctx,让插件得以调用库内真正的初始化函数,即封装一层的上面提到的 callHandler
      }
    
    // ... 其他实例方法,比如 extend,得以在业务中和Native互相约定新的非通用JSB,方便扩展
    
    1. 初始化
      导出单例appSNC,拥有的方法都在appApis中定义,如果有新的业务需求直接扩展此文件夹中内容即可。
    import * as apis from '../appApis'; // 方法集合
    import SNC from './sdk';  // 上面的 SncClass
    const option = {} ; // 一些配置
    const appSNC = new SNC(option);
    
    export default appSNC.init(apis);
    

    以上就是我们正在使用的方案,总结一下,不断积累。

    参考链接

    JSBridge深度剖析

    WebViewJavascriptBridge

    JsBridge

    相关文章

      网友评论

        本文标题:JSBridge总结

        本文链接:https://www.haomeiwen.com/subject/tdbneqtx.html