关于javascript:一文搞懂jsBridge的运行机制

43次阅读

共计 7779 个字符,预计需要花费 20 分钟才能阅读完成。

我司的 APP 是一个典型的混合开发 APP,内嵌的都是前端页面,前端页面要做到和原生的成果类似,就防止不了调用一些原生的办法,jsBridge就是 js原生 通信的桥梁,本文不讲概念性的货色,而是通过剖析一下我司我的项目中的 jsBridge 源码,来从前端角度大略理解一下它是怎么实现的。

js 调用形式

先来看一下,js是怎么来调用某个原生办法的,首先初始化的时候会调用 window.WebViewJavascriptBridge.init 办法:

window.WebViewJavascriptBridge.init()

而后如果要调用某个原生办法能够应用上面的函数:

function native (funcName, args = {}, callbackFunc, errorCallbackFunc) {
    // 校验参数是否非法
    if (args && typeof args === 'object' && Object.prototype.toString.call(args).toLowerCase() === '[object object]' && !args.length) {args = JSON.stringify(args);
    } else {throw new Error('args 不符合规范');
    }
    // 判断是否是手机环境
    if (getIsMobile()) {
        // 调用 window.WebViewJavascriptBridge 对象的 callHandler 办法
        window.WebViewJavascriptBridge.callHandler(
            funcName,
            args,
            (res) => {res = JSON.parse(res);
                if (res.code === 0) {return callbackFunc(res);
                } else {return errorCallbackFunc(res);
                }
            }
        );
    }
}

传入要调用的办法名、参数和回调即可,它先校验了一下参数,而后会调用 window.WebViewJavascriptBridge.callHandler 办法。

此外也能够提供回调供原生调用:

window.WebViewJavascriptBridge.registerHandler(funcName, callbackFunc);

接下来看一下 window.WebViewJavascriptBridge 对象到底是啥。

安卓

WebViewJavascriptBridge.js文件内是一个自执行函数,首先定义了一些变量:

// 定义变量
var messagingIframe;
var sendMessageQueue = [];// 发送音讯的队列
var receiveMessageQueue = [];// 接管音讯的队列
var messageHandlers = {};// 音讯处理器

var CUSTOM_PROTOCOL_SCHEME = 'yy';// 自定义协定
var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/';

var responseCallbacks = {};// 响应的回调
var uniqueId = 1;

依据变量名简略翻译了一下,具体用途接下来会剖析。接下来定义了 WebViewJavascriptBridge 对象:

var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
    init: init,
    send: send,
    registerHandler: registerHandler,
    callHandler: callHandler,
    _fetchQueue: _fetchQueue,
    _handleMessageFromNative: _handleMessageFromNative
};

能够看到就是一个一般的对象,下面挂载了一些办法,具体方法临时不看,持续往下:

var doc = document;
_createQueueReadyIframe(doc);

调用了 _createQueueReadyIframe 办法:

function _createQueueReadyIframe (doc) {messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    doc.documentElement.appendChild(messagingIframe);
}

这个办法很简略,就是创立了一个暗藏的 iframe 插入到页面,持续往下:

// 创立一个 Events 类型(根底事件模块)的事件(Event)对象
var readyEvent = doc.createEvent('Events');
// 定义事件名为 WebViewJavascriptBridgeReady
readyEvent.initEvent('WebViewJavascriptBridgeReady');
// 通过 document 来触发该事件
doc.dispatchEvent(readyEvent);

这里定义了一个自定义事件,并间接派发了,其余中央能够像通过监听原生事件一样监听该事件:

document.addEventListener(
    'WebViewJavascriptBridgeReady',
    function () {console.log(window.WebViewJavascriptBridge)
    },
    false
);

这里的用途我了解就是当该 jsBridge 文件如果是在其余代码之后引入的话须要保障之前的代码能晓得 window.WebViewJavascriptBridge 对象何时可用,如果规定该 jsBridge 必须要最先引入的话那么就不须要这个解决了。

到这里自执行函数就完结了,接下来看一下最开始的 init 办法:

function init (messageHandler) {if (WebViewJavascriptBridge._messageHandler) {throw new Error('WebViewJavascriptBridge.init called twice');
    }
    // init 调用的时候没有传参,所以 messageHandler=undefined
    WebViewJavascriptBridge._messageHandler = messageHandler;
    // 以后 receiveMessageQueue 也只是一个空数组
    var receivedMessages = receiveMessageQueue;
    receiveMessageQueue = null;
    for (var i = 0; i < receivedMessages.length; i++) {_dispatchMessageFromNative(receivedMessages[i]);
    }
}

从初始化的角度来看,这个 init 办法仿佛啥也没做。接下来咱们来看 callHandler 办法,看看是如何调用安卓的办法的:

function callHandler (handlerName, data, responseCallback) {
    _doSend({
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

解决了一下参数又调用了 _doSend 办法:

function _doSend (message, responseCallback) {
    // 如果提供了回调的话
    if (responseCallback) {
        // 生成一个惟一的回调 id
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
        // 回调通过 id 存储到 responseCallbacks 对象上
        responseCallbacks[callbackId] = responseCallback;
        // 把该回调 id 增加到要发送给 native 的音讯里
        message.callbackId = callbackId;
    }
    // 音讯增加到音讯队列里
    sendMessageQueue.push(message);
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
}

这个办法首先把调用原生办法时的回调函数通过生成一个惟一的 id 保留到最开始定义的 responseCallbacks 对象里,而后把该 id 增加到要发送的信息上,所以一个 message 的构造是这样的:

{
    handlerName,
    data,
    callbackId
}

接着把该 message 增加到最开始定义的 sendMessageQueue 数组里,最初设置了 iframesrc属性:yy://__QUEUE_MESSAGE__/,这其实就是一个自定义协定的 url,我简略搜寻了一下,native 会拦挡这个 url 来做相应的解决,到这里咱们就走不上来了,因为不晓得原生做了什么事件,简略搜寻了一下,发现了这个库:WebViewJavascriptBridge,我司应该是在这个库根底上批改的,联合了网上的一些文章后大略晓得了,原生拦挡到这个 url 后会调用 jswindow.WebViewJavascriptBridge._fetchQueue办法:

function _fetchQueue () {
    // 把咱们要发送的音讯队列转成字符串
    var messageQueueString = JSON.stringify(sendMessageQueue);
    // 清空音讯队列
    sendMessageQueue = [];
    // 安卓无奈间接读取返回的数据,因而还是通过 iframe 的 src 和 java 通信
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
}

安卓拦挡到 url 后,晓得 js 给安卓发送音讯了,所以被动调用 js_fetchQueue办法,取出之前增加到队列里的音讯,因为无奈间接读取 js 办法返回的数据,所以把格式化后的音讯增加到 url 上,再次通过 iframe 来发送,此时原生又会拦挡到 yy://return/_fetchQueue/ 这个 url,那么取出前面的音讯,解析出要其中要执行的原生办法名和参数后执行对应的原生办法,当原生办法执行完后又会被动调用jswindow.WebViewJavascriptBridge._handleMessageFromNative办法:

function _handleMessageFromNative (messageJSON) {
    // 依据之前的 init 办法的逻辑咱们晓得 receiveMessageQueue 是会被设置为 null 的,所以会走 else 分支
    if (receiveMessageQueue) {receiveMessageQueue.push(messageJSON);
    } else {_dispatchMessageFromNative(messageJSON);
    }
}

看一下 _dispatchMessageFromNative 办法做了什么:

function _dispatchMessageFromNative (messageJSON) {setTimeout(function () {
        // 原生发回的音讯是字符串类型的,转成 json
        var message = JSON.parse(messageJSON);
        var responseCallback;
        // java 调用实现,发回的 responseId 就是咱们之前发送给它的 callbackId
        if (message.responseId) {
            // 从 responseCallbacks 对象里取出该 id 关联的回调办法
            responseCallback = responseCallbacks[message.responseId];
            if (!responseCallback) {return;}
            // 执行回调,js 调用安卓办法后到这里顺利收到音讯
            responseCallback(message.responseData);
            delete responseCallbacks[message.responseId];
        } else {// ...}
    });
}

messageJSON就是原生发回的音讯,外面除了执行完原生办法后返回的相干信息外,还带着之前咱们传给它的 callbackId,所以咱们能够通过这个id 来在 responseCallbacks 里找到关联的回调并执行,本次 js 调用原生办法流程完结。然而,显著函数里还有不存在 id 时的分支,这里是用来干啥的呢,咱们后面介绍的都是 js 调用原生办法,然而显然,原生也能够间接给 js 发消息,比方常见的拦挡返回键性能,当原生监听到返回键事件后它会被动发送信息通知前端页面,页面就能够执行对应的逻辑,这个 else 分支就是用来解决这种状况:

function _dispatchMessageFromNative (messageJSON) {setTimeout(function () {if (message.responseId) {// ...} else {
            // 和咱们传给原生的音讯能够带 id 一样,原生传给咱们的音讯也能够带一个 id,同时原生外部也会通过这个 id 关联一个回调
            if (message.callbackId) {
                var callbackResponseId = message.callbackId;
                // 如果前端须要再给原生回音讯的话那么就带上原生之前传来的 id,这样原生就能够通过 id 找到对应的回调并执行
                responseCallback = function (responseData) {
                    _doSend({
                        responseId: callbackResponseId,
                        responseData: responseData
                    });
                };
            }
            // 咱们并没有设置默认的_messageHandler,所以是 undefined
            var handler = WebViewJavascriptBridge._messageHandler;
            // 原生发送的音讯外面有解决办法名称
            if (message.handlerName) {
                // 通过办法名称去 messageHandlers 对象里查找是否有对应的解决办法
                handler = messageHandlers[message.handlerName];
            }
            try {
                // 执行解决办法
                handler(message.data,responseCallback);
            } catch (exception) {if (typeof console !== 'undefined') {console.log('WebViewJavascriptBridge: WARNING: javascript handler threw.', message, exception);
                }
            }
        }
    });
}

比方咱们要监听原生的返回键事件,咱们先通过 window.WebViewJavascriptBridge 对象的办法注册一下:

window.WebViewJavascriptBridge.registerHandler('onBackPressed', () => {// 做点什么...})

registerHandler办法如下:

function registerHandler (handlerName, handler) {messageHandlers[handlerName] = handler;
}

很简略,把咱们要监听的事件名和办法都存储到 messageHandlers 对象上,而后如果原生监听到返回键事件后会发送如下构造的音讯:

{handlerName: 'onBackPressed'}

这样就能够通过 handlerName 找到咱们注册的函数进行执行了。

到此,安卓环境的 js 和原生相互调用的逻辑就完结了,总结一下就是:

1.js调用原生

生成一个惟一的 id,把回调和id 保存起来,而后将要发送的信息(带上本次生成的惟一 id)增加到一个队列里,之后通过 iframe 发送一个自定义协定的申请,原生拦挡到后调用 jswindow.WebViewJavascriptBridge对象的一个办法来获取队列的信息,解析出申请和参数后执行对应的原生办法,而后再把响应(带上前端传来的 id)通过调用 jswindow.WebViewJavascriptBridge的指定办法传递给前端,前端再通过 id 找到之前存储的回调,进行执行。

2. 原生调用js

首先前端须要当时注册要监听的事件,把事件名和回调保存起来,而后原生在某个时刻会调用 jswindow.WebViewJavascriptBridge对象的指定办法,前端依据返回参数的事件名找到注册的回调进行执行,同时原生也会传过来一个 id,如果前端执行完相应逻辑后还要给原生回音讯,那么要把该id 带回去,原生依据该 id 来找到对应的回调进行执行。

能够看到,js和原生两边的逻辑都是统一的。

ios

ios和安卓根本是统一的,局部细节上有点区别,首先是协定不一样,ios的是这样的:

var CUSTOM_PROTOCOL_SCHEME_IOS = 'https';
var QUEUE_HAS_MESSAGE_IOS = '__wvjb_queue_message__';

而后 ios 初始化创立 iframe 的时候会发送一个申请:

var BRIDGE_LOADED_IOS = '__bridge_loaded__';
function _createQueueReadyIframe (doc) {messagingIframe = doc.createElement('iframe');
    messagingIframe.style.display = 'none';
    if (isIphone()) {
        // 这里应该是 ios 须要先加载一下 bridge
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME_IOS + '://' + BRIDGE_LOADED_IOS;
    }
    doc.documentElement.appendChild(messagingIframe);
}

再而后是 ios 获取咱们的音讯队列时不须要通过 iframe,它能间接获取执行js 函数返回的数据:

function _fetchQueue () {var messageQueueString = JSON.stringify(sendMessageQueue);
    sendMessageQueue = [];
    return messageQueueString;// 间接返回,不须要通过 iframe
}

其余局部都是一样的。

总结

本文剖析了一下 jsBridge 的源码,能够发现其实是个很简略的货色,然而平时可能就没有去认真理解过它,总想做一些”大“的事件,以至于沦为了一个”好高骛远“的人,心愿各位不要像笔者一样。

另外本文剖析的只是笔者公司的 jsBridge 实现,可能有不一样、更好或更新的实现,欢送留言探讨。

正文完
 0