我司的APP是一个典型的混合开发APP,内嵌的都是前端页面,前端页面要做到和原生的效果相似,就避免不了调用一些原生的方法,jsBridge就是js原生通信的桥梁,本文不讲概念性的东西,而是通过分析一下我司项目中的jsBridge源码,来从前端角度大概了解一下它是怎么实现的。

js调用方式

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

  1. window.WebViewJavascriptBridge.init()

然后如果要调用某个原生方法可以使用下面的函数:

  1. function native (funcName, args = {}, callbackFunc, errorCallbackFunc) {
  2. // 校验参数是否合法
  3. if (args && typeof args === 'object' && Object.prototype.toString.call(args).toLowerCase() === '[object object]' && !args.length) {
  4. args = JSON.stringify(args);
  5. } else {
  6. throw new Error('args不符合规范');
  7. }
  8. // 判断是否是手机环境
  9. if (getIsMobile()) {
  10. // 调用window.WebViewJavascriptBridge对象的callHandler方法
  11. window.WebViewJavascriptBridge.callHandler(
  12. funcName,
  13. args,
  14. (res) => {
  15. res = JSON.parse(res);
  16. if (res.code === 0) {
  17. return callbackFunc(res);
  18. } else {
  19. return errorCallbackFunc(res);
  20. }
  21. }
  22. );
  23. }
  24. }

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

此外也可以提供回调供原生调用:

  1. window.WebViewJavascriptBridge.registerHandler(funcName, callbackFunc);

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

安卓

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

  1. // 定义变量
  2. var messagingIframe;
  3. var sendMessageQueue = [];// 发送消息的队列
  4. var receiveMessageQueue = [];// 接收消息的队列
  5. var messageHandlers = {};// 消息处理器
  6. var CUSTOM_PROTOCOL_SCHEME = 'yy';// 自定义协议
  7. var QUEUE_HAS_MESSAGE = '__QUEUE_MESSAGE__/';
  8. var responseCallbacks = {};// 响应的回调
  9. var uniqueId = 1;

根据变量名简单翻译了一下,具体用处接下来会分析。接下来定义了WebViewJavascriptBridge对象:

  1. var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
  2. init: init,
  3. send: send,
  4. registerHandler: registerHandler,
  5. callHandler: callHandler,
  6. _fetchQueue: _fetchQueue,
  7. _handleMessageFromNative: _handleMessageFromNative
  8. };

可以看到就是一个普通的对象,上面挂载了一些方法,具体方法暂时不看,继续往下:

  1. var doc = document;
  2. _createQueueReadyIframe(doc);

调用了_createQueueReadyIframe方法:

  1. function _createQueueReadyIframe (doc) {
  2. messagingIframe = doc.createElement('iframe');
  3. messagingIframe.style.display = 'none';
  4. doc.documentElement.appendChild(messagingIframe);
  5. }

这个方法很简单,就是创建了一个隐藏的iframe插入到页面,继续往下:

  1. // 创建一个Events类型(基础事件模块)的事件(Event)对象
  2. var readyEvent = doc.createEvent('Events');
  3. // 定义事件名为WebViewJavascriptBridgeReady
  4. readyEvent.initEvent('WebViewJavascriptBridgeReady');
  5. // 通过document来触发该事件
  6. doc.dispatchEvent(readyEvent);

这里定义了一个自定义事件,并直接派发了,其他地方可以像通过监听原生事件一样监听该事件:

  1. document.addEventListener(
  2. 'WebViewJavascriptBridgeReady',
  3. function () {
  4. console.log(window.WebViewJavascriptBridge)
  5. },
  6. false
  7. );

这里的用处我理解就是当该jsBridge文件如果是在其他代码之后引入的话需要保证之前的代码能知道window.WebViewJavascriptBridge对象何时可用,如果规定该jsBridge必须要最先引入的话那么就不需要这个处理了。

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

  1. function init (messageHandler) {
  2. if (WebViewJavascriptBridge._messageHandler) {
  3. throw new Error('WebViewJavascriptBridge.init called twice');
  4. }
  5. // init调用的时候没有传参,所以messageHandler=undefined
  6. WebViewJavascriptBridge._messageHandler = messageHandler;
  7. // 当前receiveMessageQueue也只是一个空数组
  8. var receivedMessages = receiveMessageQueue;
  9. receiveMessageQueue = null;
  10. for (var i = 0; i < receivedMessages.length; i++) {
  11. _dispatchMessageFromNative(receivedMessages[i]);
  12. }
  13. }

从初始化的角度来看,这个init方法似乎啥也没做。接下来我们来看callHandler方法,看看是如何调用安卓的方法的:

  1. function callHandler (handlerName, data, responseCallback) {
  2. _doSend({
  3. handlerName: handlerName,
  4. data: data
  5. }, responseCallback);
  6. }

处理了一下参数又调用了_doSend方法:

  1. function _doSend (message, responseCallback) {
  2. // 如果提供了回调的话
  3. if (responseCallback) {
  4. // 生成一个唯一的回调id
  5. var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
  6. // 回调通过id存储到responseCallbacks对象上
  7. responseCallbacks[callbackId] = responseCallback;
  8. // 把该回调id添加到要发送给native的消息里
  9. message.callbackId = callbackId;
  10. }
  11. // 消息添加到消息队列里
  12. sendMessageQueue.push(message);
  13. messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
  14. }

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

  1. {
  2. handlerName,
  3. data,
  4. callbackId
  5. }

接着把该message添加到最开始定义的sendMessageQueue数组里,最后设置了iframesrc属性:yy://__QUEUE_MESSAGE__/,这其实就是一个自定义协议的url,我简单搜索了一下,native会拦截这个url来做相应的处理,到这里我们就走不下去了,因为不知道原生做了什么事情,简单搜索了一下,发现了这个库:WebViewJavascriptBridge,我司应该是在这个库基础上修改的,结合了网上的一些文章后大概知道了,原生拦截到这个url后会调用jswindow.WebViewJavascriptBridge._fetchQueue方法:

  1. function _fetchQueue () {
  2. // 把我们要发送的消息队列转成字符串
  3. var messageQueueString = JSON.stringify(sendMessageQueue);
  4. // 清空消息队列
  5. sendMessageQueue = [];
  6. // 安卓无法直接读取返回的数据,因此还是通过iframe的src和java通信
  7. messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
  8. }

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

  1. function _handleMessageFromNative (messageJSON) {
  2. // 根据之前的init方法的逻辑我们知道receiveMessageQueue是会被设置为null的,所以会走else分支
  3. if (receiveMessageQueue) {
  4. receiveMessageQueue.push(messageJSON);
  5. } else {
  6. _dispatchMessageFromNative(messageJSON);
  7. }
  8. }

看一下_dispatchMessageFromNative方法做了什么:

  1. function _dispatchMessageFromNative (messageJSON) {
  2. setTimeout(function () {
  3. // 原生发回的消息是字符串类型的,转成json
  4. var message = JSON.parse(messageJSON);
  5. var responseCallback;
  6. // java调用完成,发回的responseId就是我们之前发送给它的callbackId
  7. if (message.responseId) {
  8. // 从responseCallbacks对象里取出该id关联的回调方法
  9. responseCallback = responseCallbacks[message.responseId];
  10. if (!responseCallback) {
  11. return;
  12. }
  13. // 执行回调,js调用安卓方法后到这里顺利收到消息
  14. responseCallback(message.responseData);
  15. delete responseCallbacks[message.responseId];
  16. } else {
  17. // ...
  18. }
  19. });
  20. }

messageJSON就是原生发回的消息,里面除了执行完原生方法后返回的相关信息外,还带着之前我们传给它的callbackId,所以我们可以通过这个id来在responseCallbacks里找到关联的回调并执行,本次js调用原生方法流程结束。但是,明显函数里还有不存在id时的分支,这里是用来干啥的呢,我们前面介绍的都是js调用原生方法,但是显然,原生也可以直接给js发消息,比如常见的拦截返回键功能,当原生监听到返回键事件后它会主动发送信息告诉前端页面,页面就可以执行对应的逻辑,这个else分支就是用来处理这种情况:

  1. function _dispatchMessageFromNative (messageJSON) {
  2. setTimeout(function () {
  3. if (message.responseId) {
  4. // ...
  5. } else {
  6. // 和我们传给原生的消息可以带id一样,原生传给我们的消息也可以带一个id,同时原生内部也会通过这个id关联一个回调
  7. if (message.callbackId) {
  8. var callbackResponseId = message.callbackId;
  9. //如果前端需要再给原生回消息的话那么就带上原生之前传来的id,这样原生就可以通过id找到对应的回调并执行
  10. responseCallback = function (responseData) {
  11. _doSend({
  12. responseId: callbackResponseId,
  13. responseData: responseData
  14. });
  15. };
  16. }
  17. // 我们并没有设置默认的_messageHandler,所以是undefined
  18. var handler = WebViewJavascriptBridge._messageHandler;
  19. // 原生发送的消息里面有处理方法名称
  20. if (message.handlerName) {
  21. // 通过方法名称去messageHandlers对象里查找是否有对应的处理方法
  22. handler = messageHandlers[message.handlerName];
  23. }
  24. try {
  25. // 执行处理方法
  26. handler(message.data responseCallback);
  27. } catch (exception) {
  28. if (typeof console !== 'undefined') {
  29. console.log('WebViewJavascriptBridge: WARNING: javascript handler threw.', message, exception);
  30. }
  31. }
  32. }
  33. });
  34. }

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

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

registerHandler方法如下:

  1. function registerHandler (handlerName, handler) {
  2. messageHandlers[handlerName] = handler;
  3. }

很简单,把我们要监听的事件名和方法都存储到messageHandlers对象上,然后如果原生监听到返回键事件后会发送如下结构的消息:

  1. {
  2. handlerName: 'onBackPressed'
  3. }

这样就可以通过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的是这样的:

  1. var CUSTOM_PROTOCOL_SCHEME_IOS = 'https';
  2. var QUEUE_HAS_MESSAGE_IOS = '__wvjb_queue_message__';

然后ios初始化创建iframe的时候会发送一个请求:

  1. var BRIDGE_LOADED_IOS = '__bridge_loaded__';
  2. function _createQueueReadyIframe (doc) {
  3. messagingIframe = doc.createElement('iframe');
  4. messagingIframe.style.display = 'none';
  5. if (isIphone()) {
  6. // 这里应该是ios需要先加载一下bridge
  7. messagingIframe.src = CUSTOM_PROTOCOL_SCHEME_IOS + '://' + BRIDGE_LOADED_IOS;
  8. }
  9. doc.documentElement.appendChild(messagingIframe);
  10. }

再然后是ios获取我们的消息队列时不需要通过iframe,它能直接获取执行js函数返回的数据:

  1. function _fetchQueue () {
  2. var messageQueueString = JSON.stringify(sendMessageQueue);
  3. sendMessageQueue = [];
  4. return messageQueueString;// 直接返回,不需要通过iframe
  5. }

其他部分都是一样的。

总结

本文分析了一下jsBridge的源码,可以发现其实是个很简单的东西,但是平时可能就没有去认真了解过它,总想做一些”大“的事情,以至于沦为了一个”好高骛远“的人,希望各位不要像笔者一样。

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

一文搞懂jsBridge的运行机制的更多相关文章

  1. 一文搞懂Flink Window机制

    Windows是处理无线数据流的核心,它将流分割成有限大小的桶(buckets),并在其上执行各种计算. 窗口化的Flink程序的结构通常如下,有分组流(keyed streams)和无分组流(non ...

  2. 一文搞懂RAM、ROM、SDRAM、DRAM、DDR、flash等存储介质

    一文搞懂RAM.ROM.SDRAM.DRAM.DDR.flash等存储介质 存储介质基本分类:ROM和RAM RAM:随机访问存储器(Random Access Memory),易失性.是与CPU直接 ...

  3. 一文搞懂所有Java集合面试题

    Java集合 刚刚经历过秋招,看了大量的面经,顺便将常见的Java集合常考知识点总结了一下,并根据被问到的频率大致做了一个标注.一颗星表示知识点需要了解,被问到的频率不高,面试时起码能说个差不多.两颗 ...

  4. 一文搞懂指标采集利器 Telegraf

    作者| 姜闻名 来源|尔达 Erda 公众号 ​ 导读:为了让大家更好的了解 MSP 中 APM 系统的设计实现,我们决定编写一个<详聊微服务观测>系列文章,深入 APM 系统的产品.架构 ...

  5. 三文搞懂学会Docker容器技术(中)

    接着上面一篇:三文搞懂学会Docker容器技术(上) 6,Docker容器 6.1 创建并启动容器 docker run [OPTIONS] IMAGE [COMMAND] [ARG...] --na ...

  6. 三文搞懂学会Docker容器技术(下)

    接着上面一篇:三文搞懂学会Docker容器技术(上) 三文搞懂学会Docker容器技术(中) 7,Docker容器目录挂载 7.1 简介 容器目录挂载: 我们可以在创建容器的时候,将宿主机的目录与容器 ...

  7. 一文搞懂如何使用Node.js进行TCP网络通信

    摘要: 网络是通信互联的基础,Node.js提供了net.http.dgram等模块,分别用来实现TCP.HTTP.UDP的通信,本文主要对使用Node.js的TCP通信部份进行实践记录. 本文分享自 ...

  8. 基础篇|一文搞懂RNN(循环神经网络)

    基础篇|一文搞懂RNN(循环神经网络) https://mp.weixin.qq.com/s/va1gmavl2ZESgnM7biORQg 神经网络基础 神经网络可以当做是能够拟合任意函数的黑盒子,只 ...

  9. 一文搞懂 Prometheus 的直方图

    原文链接:一文搞懂 Prometheus 的直方图 Prometheus 中提供了四种指标类型(参考:Prometheus 的指标类型),其中直方图(Histogram)和摘要(Summary)是最复 ...

随机推荐

  1. Java学习day4

    今天学习了String以及StringBuider 其中较为重点的除了用法以外,还有,String对象以""方式给出的字符串,只要字符序列相同(即顺序和大小写)无论在代码中重复几次 ...

  2. 那些年uniapp踩过的坑之-------搜索框插件uni-search-bar字体和图标居中的问题

    用uniapp必不可少的就是搜索框 但是公司要求的是这样滴 但是 uni-search-bar这个插件给我的偏偏是这样子滴 这个时候我以为是简简单单的样式问题,但是多方调试无果之后才发现,这两个根本不 ...

  3. 图片叠加科幻切换 -- css

    <template> <div>   <div class="a"></div> <div class="b&quo ...

  4. 我用 CSS3 实现了一个超炫的 3D 加载动画

    今天给大家带来一个非常炫酷的CSS3加载Loading动画,它的特别之处在于,整个Loading动画呈现出了3D的视觉效果.这个Loading加载动画由12个3D圆柱体围成一个椭圆形,同时这12个圆柱 ...

  5. Delphi 类库(DLL)动态调用与静态调用示例讲解

    在Delphi或者其它程序中我们经常需要调用别人写好的DLL类库,下面直接上示例代码演示如何进行动态和静态的调用方法: { ************************************** ...

  6. git冲突解决、线上分支合并、luffy项目后台登陆注册页面分析引入

    今日内容概要 git冲突解决 线上分支合并 登陆注册页面(引入) 手机号是否存在接口 腾讯云短信申请 内容详细 1.git冲突解决 1.1 多人在同一分支开发,出现冲突 # 先将前端项目也做上传到 g ...

  7. redis的zset数据结构:跳表

    点赞再看,养成习惯,微信搜索「小大白日志」关注这个搬砖人. 文章不定期同步公众号,还有各种一线大厂面试原题.我的学习系列笔记. 广州这边封闭式管理好久了,今天终于周末可以出去溜溜了 什么是zset z ...

  8. ChCore Lab2 内存管理 实验笔记

    本文为上海交大 ipads 研究所陈海波老师等人所著的<现代操作系统:原理与实现>的课程实验(LAB)的学习笔记的第二篇.所有章节的笔记可在此处查看:chcore | 康宇PL's Blo ...

  9. iNeuOS工业互联网操作系统,数据点、设备和业务的计算与预警

    目       录 1.      概述... 2 2.      概念解释... 2 3.      数据点的计算与预警... 2 4.      设备的计算与预警... 3 5.      业务的 ...

  10. Nginx编译安装及常用命令

    一个执着于技术的公众号 前言 前面我们已经了解Nginx基础入门知识,今天就带大家一起学习下Nginx编译安装部署 准备工作 一台linux机器(本次实验以CentOS 7.5为例) 到Nginx官方 ...