浅析H5与app通信桥梁JsBridge的原理

记录正在使用的JsBridge的实现原理,方便更好地定位问题。

Posted by ddxg on October 16, 2019

浅析H5与app通信桥梁JsBridge的原理

移动端的混合应用是非常常见的,Hybrid APP、React Native APP或者Flutter都很火爆,开发成本相对较低,迭代快,性能也越来越好。

公司产品的app中也是内嵌了很多H5页面,使用JsBridge进行通信。这里简要分析一下它的原理。

拿到ios app中相关的代码之后,我发现公司使用的是 marcuswestin/WebViewJavascriptBridge 这个开源技术,已经有两年没有更新了,不过项目用得还是挺好的,只是发现安卓上面有一个json字符串转义的小问题。这个开源技术是使用URL scheme的方式实现H5与app通信的,没有兼容问题。还有一种通信方式是Api 直接交互,H5调原生和原生调H5都有提供专门的api,就是一些低版本不支持。

我讲的内容可能不是很全面,可以直接参考这篇文章WebViewJavascriptBridge原理解析

我收集的关键代码

主要流程

这里主要讲解H5页面中使用的 jsBridge.js 文件 和 APP中保留的 WebViewJavascriptBridge.js 文件。Native方面的代码不太懂,所以就不多讲了。

核心部分

核心部分有三个:

  • H5 页面中的 jsBridge.js 文件,主要是js桥相关业务逻辑的封装
  • APP中保留的一份 WebViewJavascriptBridge.js 文件,最重要的部分,定义全局方法供H5和Native是哟ing。负责接收app发给javascript的消息,并且把javascript环境的消息发送给app
  • APP 接受javascript环境发过来的消息,处理完之后返回处理结果,主要是app的功能代码

这三部分的关系如下图: jsBridge关系图

H5 页面中的 jsBridge.js 文件

这一部分还是比较简单的,主要是发起 iframe.src='xxx' 通知Native向webview中注入WebViewJavascriptBridge.js 和 使用 WebViewJavascriptBridge.registerHandlerWebViewJavascriptBridge.callHandler 注册、执行原生方法。

关键代码:

// window.WebViewJavascriptBridge对象是 客户端 注入到webview的window全局环境中的
if (window[JS_BRIDGE_NAME]) {
    _callback(window[JS_BRIDGE_NAME]);
}
else {
    // window.WVJBCallbacks 是事件队列
    if (window[MESSAGE_QUEUE_NAME]) {
        window[MESSAGE_QUEUE_NAME].push(_callback);
    } else {
        // 在window.WebViewJavascriptBridge没有挂载之前将调用了的js-bridge方法暂时存储到一个队列中
        window[MESSAGE_QUEUE_NAME] = [_callback];
        /**
            * 这个iframe的作用是 加载并执行 保存在app端的WebViewJavascriptBridge_JS.js文件中的代码,执行完之后在全局放一个window.WebViewJavascriptBridge对象
            */
        let WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function () {
            document.documentElement.removeChild(WVJBIframe);
        }, 0);
    }
}


function _call() {
    /**
     * cmd:web端与app端定好的实现功能的名字,如:cmd=login,web端调用之后app就会处理登陆逻辑
     * params:web端传给app端的参数
     * callback:这个方法回来之后web端要执行的逻辑
     */
    let [cmd, params, callback] = parseCallArguments(arguments);

    // 这个bridge就是window.WebViewJavascriptBridge,这里使用里面的callHandler方法
    _run(bridge => {
        bridge.callHandler(cmd, ...params, callback);
    });
}

function _register(cmd, callback) {
    _run(bridge => {
        bridge.registerHandler(cmd, callback);
    });
}

// 提供两个方法给具体业务调用
module.exports = {
    _call,
    _register
};

APP中的 WebViewJavascriptBridge.js 文件

这个文件是最核心的部分,是H5与app通信的桥梁。重要的操作时定义供H5和Nativ调用的全局方法,处理Native发送过来的消息 和 发送消息给Native

WebViewJavascriptBridge全局方法:

// 在全局定义window.WebViewJavascriptBridge对象,供H5调用
window.WebViewJavascriptBridge = {
    registerHandler: registerHandler, // 注册事件,H5调用
    callHandler: callHandler, // 执行事件,H5调用
    disableJavscriptAlertBoxSafetyTimeout: disableJavscriptAlertBoxSafetyTimeout, // 设置 与app通信 不需要需要延迟
    _fetchQueue: _fetchQueue, // 获取存储事件数据队列json字符串,app调用
    _handleMessageFromObjC: _handleMessageFromObjC // app调用JS的入口方法,app调用
};

处理Native发过来的消息:

其实就是Native调用 window.WebViewJavascriptBridge._handleMessageFromObjC 方法,主要的处理逻辑如下:代码中涉及到一些 responseIdcallbackId 都是处理回调的一些标识,方便找到哪个消息对应调用哪个回调,具体的逻辑可能要仔细看代码才能知道,具体代码都在marcuswestin/WebViewJavascriptBridge中,我这里就不具体解释了。

// app 给 js 通信,处理从app返回的消息
function _dispatchMessageFromObjC(messageJSON) {
    if (dispatchMessagesWithTimeoutSafety) {
        setTimeout(_doDispatchMessageFromObjC);
    } else {
            _doDispatchMessageFromObjC();
    }
    
    function _doDispatchMessageFromObjC() {

        // message = { handlerName, data, callbackId }
        var message = JSON.parse(messageJSON);
        var messageHandler;
        var responseCallback;

        // 如果app返回的数据有responseId说明app已经处理好了这个事件
        // 处理通过callHandler方法的传递的消息回调
        if (message.responseId) {
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {
                return;
            }
            // 直接执行回调
            responseCallback(message.responseData);
            // 执行完后删除回调队列中的方法
            delete responseCallbacks[message.responseId];
        } else {
            //  处理通过registerHandler方法的注册事件
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                responseCallback = function(responseData) {
                    _doSend({ handlerName:message.handlerName, responseId:callbackResponseId, responseData:responseData });
                };
            }
            
            // 处理注册事件
            var handler = messageHandlers[message.handlerName];
            if (!handler) {
                console.log("WebViewJavascriptBridge: WARNING: no handler for message from ObjC:", message);
            } else {
                handler(message.data, responseCallback);
            }
        }
    }
}

发送消息给Native:

这里发送消息给Native也是使用Url scheme的方式,只是连接变了。发送消息之前都会把H5传过来的消息存到 sendMessageQueue 这个数组中,Native监听到有消息发送过来就会调用 WebViewJavascriptBridge._fetchQueue 这个方法获取 sendMessageQueue 中的所有消息

/**
* js 给 app 发送消息
* @param {Object} message 事件数据,{ handlerName:handlerName, data:data },cmd + data
* @param {Function} responseCallback H5传过来的回调函数
*/
function _doSend(message, responseCallback) {
    if (responseCallback) {
        var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime();
        // 存储responseCallback回调函数
        responseCallbacks[callbackId] = responseCallback;
        // 给message多加一个callbackId属性,message事件数据的id 和 回调函数的id是一致的,这方便app处理完之后执行回调函数
        message['callbackId'] = callbackId;
    }
    // 存储 事件数据
    sendMessageQueue.push(message);
    // 与app通信的方式,会触发decidePolicyForNavigationAction  webview跳转代理。
    // 简单的讲就是app会监控下面连接的跳转,当检测到这个跳转app就知道是H5给app传递了消息事件(调用了callHandler方法),
    // app就会通过全局_fetchQueue和_handleMessageFromObjC方法处理事件。因为app是可以通过window.WebViewJavascriptBridge调用js方法的。
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

Native获取到js发送过来的消息后的处理

Native 中会使用flushMessageQueue这个方法处理消息,它的处理逻辑跟 WebViewJavascriptBridge._handleMessageFromObjC 是一致的。代码在这里WebViewJavascriptBridge/WebViewJavascriptBridge/WebViewJavascriptBridgeBase.m

我这里主要想说的是,H5和Native两端各自维护同一个功能列表,是一一对应的。举个例子:

H5这边定义了两个js桥方法:logingetUserInfo,调用后的处理方式H5维护。然后Native这边也需要定义对应的两个 logingetUserInfo 方法,这两个方法的具体执行逻辑是Native维护。

也就是H5调用了一个js桥后,Native必须要有对应的处理,否则就会调用失败。

对于注册的js桥方法也是一样,这些方法都是Native主动触发的,如果H5没有处理那H5就不会有任何反馈。

所以对于js桥来说,增加或者修改一个桥是两端都要一起完成的事情,需要一起定义传参、测试和联调。

总结

urlScheme方式的js桥还是挺好理解的,使用marcuswestin/WebViewJavascriptBridge的过程中还是会发现一些bug吧,这种源码级别的bug还真的很棘手呀,作者也有两年多没有维护了,我看github上还有很多人反馈bug呢。随着事件的推进肯定会遇到更多的bug,如果完全吃透源码的话,其实我们可以自己完善代码解决bug。

参考: