JsBridge源码剖析

JsBridge是Hybrid项目中一种实现H5与Native两者之间通讯的成熟、安全的解决方案。

基础原理

JsBridge,顾名思义,它是一座桥,架在H5和Native之间的桥;通过这座桥,H5这边可以调用原生的方法,比如调用Native的分享、相机等等这些H5无法很好满足的功能;同时,Native也可以通过这座桥,更为方便、规范地使用约定好的方法。
那JsBridge的通讯机理是什么呢?其实就是url scheme的触发与拦截。

H5调用Native

JsBridge中定义好一套url scheme协议,H5端根据协议触发响应的url,页面载体webview拦截住url后,解析发现是约定好的通讯协议,Native根据解析结果调用响应的原生方法,进行响应的操作。

Native调用H5

那么,Native是怎么调用H5中的方法的呢?不好意思,是Native创建了H5的载体webview,所以Native是爸爸,它可以直接调用页面中的全局函数方法。但是既然使用了JsBridge,那么肯定是要按照协议执行的,“执法办事”是必要的,不然代码没有规范,难以落成文档,后期迭代维护成本会递增。
在使用JsBridge的情况下,JsBridge将会挂载在页面的window变量中,H5将Native所需调用的方法注册到JsBridge中;Native然后根据约定的方式对这些方法进行传参和调用,具体实现会在下面源码解析板块进行解读。

源码剖析

JsBridge的源码不多,不到200行,下面我们按照“自己手写一个JsBridge”的思路来对源码进行搜索拆分。

H5调用Native

根据事先机理,我首先想到的是,把实现H5调用Native的代码捋出来。那么,我们需要在页面中来触发url scheme。我们会想到window.location.href、a标签或者iframe等等。源码中使用的是iframe.src来触发,为什么呢?因为window.location.href和a标签其实是一样,其如果短时间内触发多次,webview只会捕获最后一次请求而忽略之前的 ,这个解释援引搜索内容,我没有去进行验证,有兴趣的同学可以验证下哦。下面先看第一段代码,描述我就直接写在在代码的注释中了。

var messagingIframe; // 触发url scheme的iframe
var bizMessagingIframe; // 又定义了一个,这个我们不管,再往下看看
var sendMessageQueue = []; // 存放H5向Native发送的消息队列
var receiveMessageQueue = []; // 存放Native发送给H5的消息
var messageHandlers = {};  // 可供native调用的方法,通过registerHandler注册存入messageHandlers

var CUSTOM_PROTOCOL_SCHEME = 'yy'; // url scheme中用以标志协议的字段
var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/'; // url scheme中另一个字段

var responseCallbacks = {}; // 存放callback的对象
var uniqueId = 1; // 用于生成callbackId的标志之一,每次生成后都会加1

有了进行通讯的url,我们下面就来看看是怎么进行触发的吧。挂载window上的WebViewJavascriptBridge中定义一些方法,callHandler就是用来给H5调用Native使用的。

var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
    init: init,  // 初始化
    send: send, // 单纯的H5发送消息
    registerHandler: registerHandler, // 用以注册Native需要调用的js方法
    callHandler: callHandler, // 发送消息,并指明需要调用的Native方法
    _fetchQueue: _fetchQueue, //  Native调用,用以获取sendMessageQueue中的消息
    _handleMessageFromNative: _handleMessageFromNative // Native调用,给H5发消息
};

callHandler只是对_doSend函数的简单封装,看来具体的实现是在_doSend中

function callHandler(handlerName, data, responseCallback) {
    _doSend({ // 发送的message包含调用的native方法名称以及传输的数据
        handlerName: handlerName,
        data: data
    }, responseCallback);
}

/**
 * [_doSend H5调用native,对callHandler的丰富]
 * @param       {[type]} message          [调用消息体]
 * @param       {[type]} responseCallback [回调函数]
 * @constructor
 * @return      {[type]}                  [description]
 */
function _doSend(message, responseCallback) {
  // 如果传入回调函数,则为回调函数生成一个callbackId,
  // 以callbackId为key,将回调函数存入responseCallbacks对象中,用于之后回调使用
    if (responseCallback) {
        var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime(); // callbackId
        responseCallbacks[callbackId] = responseCallback; // 将callbackId维护字在responseCallbacks中
        message.callbackId = callbackId; // message新加一个callbackId字段
        // 当前message包含内容
        // {
        //   callbackId: callbackId, // 回调函数id
        //   handlerName: handlerName, // 调用的Native方法名
        //   data: data // 传参数据
        // }
    }
    sendMessageQueue.push(message);
    // 通过iframe通知客户端有消息了
    messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE; 
}

这边我们便可以来解释下最初为啥要创建两个iframe。messagingIframe并不是用来传输消息的,而只是告诉Native有消息需要处理了,真正用来传输消息的iframe是bizMessagingIframe。

// 提供给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);
    }
}

Native得到通知后,通过_fetchQueue方法将sendMessageQueue中消息取出并通过bizMessagingIframe触发url scheme来传输消息。说实话,这边的实现有点迷糊,感觉没必要多中间通知这个环节,可能作者另有考虑吧。

Native调用H5

WebViewJavascriptBridge中定义的_handleMessageFromNative就是用来处理Native对H5的调用

// Native调用H5主要分为两种
// 1、H5调用Native时,传入的callback,Native执行完后,现在来执行回调
// 2、Native调用H5方法,可以选择传入需要回调的Native函数
function _dispatchMessageFromNative(messageJSON) {
    setTimeout(function() { // 异步挂起处理,不会影响同步任务
        var message = JSON.parse(messageJSON);
        var responseCallback;
        if (message.responseId) { // 如果消息中有responseId,代表这是native执行完,这个responseId其实就是_doSend函数中传入的callbackId
            responseCallback = responseCallbacks[message.responseId]; // 根据responseId取出之前存入responseCallbacks对象中的回调函数
            if (!responseCallback) {
                return;
            }
            responseCallback(message.responseData); // 传入message.responseData参数,执行callback
            delete responseCallbacks[message.responseId]; // callback执行完后,从responseCallbacks队列中删除该callback
        } else { // native调用H5的方法
        // Native调用H5方法过程与之前类似,也可以要求有个回调
            if (message.callbackId) { // callbackId代表,native要求H5方法执行完后,给native一个回调
                var callbackResponseId = message.callbackId;
                responseCallback = function(responseData) {
                    _doSend({
                        responseId: callbackResponseId, // responseId告诉native这是你要的回调
                        responseData: responseData
                    });
                };
            }

            var handler = WebViewJavascriptBridge._messageHandler; // init时定义的H5默认方法,如果native没有指明handlerName的情况下,就会调用默认方法
            if (message.handlerName) { // native指定了要调用H5方法
                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);
                }
            }
        }
    });
}

至此,JsBridge中H5与Native之间通讯的主体实现代码就已经讲完了。还是蛮简单的,其实就是定义了两者通讯的一种协议。

最后

年后本想着写一篇文章来阐述Hybrid实现原理的完整技术解读,奈何在原生部分有短板,就只能写个JsBridge来个源码解析来解解馋了~

参考

  1. JSBridge的原理与实现
  2. WebViewJavascriptBridge.js
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343

推荐阅读更多精彩内容