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

一篇文章学会jsBridge的运行机制

程序员文章站 2022-03-09 09:05:36
目录我司的app是一个典型的混合开发app,内嵌的都是前端页面,前端页面要做到和原生的效果相似,就避免不了调用一些原生的方法,jsbridge就是js和原生通信的桥梁,本文不讲概念性的东西,而是通过分...

我司的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的运行机制的文章就介绍到这了,更多相关jsbridge 运行机制内容请搜索以前的文章或继续浏览下面的相关文章希望大家以后多多支持!