说明

JSBridge实现原理

目录

前言

参考来源

前人栽树,后台乘凉,本文参考了以下来源

前置技术要求

阅读本文前,建议先阅读以下文章

楔子

上文中简单的介绍了JSBridge,以及为什么要用JSBridge,本文详细介绍它的实现原理

原理概述

简介

JSBridge是Native代码与JS代码的通信桥梁。目前的一种统一方案是:H5触发url scheme->Native捕获url scheme->原生分析,执行->原生调用h5。如下图

url scheme介绍

上图中有提到url scheme这个概念,那这到底是什么呢?

url scheme是一种类似于url的链接,是为了方便app直接互相调用设计的

具体为,可以用系统的OpenURI打开一个类似于url的链接(可拼入参数),然后系统会进行判断,如果是系统的url scheme,则打开系统应用,否则找看是否有app注册这种scheme,打开对应app

需要注意的是,这种scheme必须原生app注册后才会生效,如微信的scheme为(weixin://)

而本文JSBridge中的url scheme则是仿照上述的形式的一种方式

具体为,app不会注册对应的scheme,而是由前端页面通过某种方式触发scheme(如用iframe.src),然后Native用某种方法捕获对应的url触发事件,然后拿到当前的触发url,根据定义好的协议,分析当前触发了那种方法,然后根据定义来执行等

实现流程

基于上述的基本原理,现在开始设计一种JSBridge的实现

实现思路

要实现JSBridge,我们可以进行关键步骤分析

第一步:设计出一个Native与JS交互的全局桥对象

第二步:JS如何调用Native

第三步:Native如何得知api被调用

第四步:分析url-参数和回调的格式

第五步:Native如何调用JS

第六步:H5中api方法的注册以及格式

如下图:

第一步:设计出一个Native与JS交互的全局桥对象

我们规定,JS和Native之间的通信必须通过一个H5全局对象JSbridge来实现,该对象有如下特点

该对象名为"JSBridge",是H5页面中全局对象window的一个属性

var JSBridge = window.JSBridge || (window.JSBridge = {});

该对象有如下方法

registerHandler( String,Function )H5调用 注册本地JS方法,注册后Native可通过JSBridge调用。调用后会将方法注册到本地变量messageHandlers 中

callHandler( String,JSON,Function )H5调用 调用原生开放的api,调用后实际上还是本地通过url scheme触发。调用时会将回调id存放到本地变量responseCallbacks中

_handleMessageFromNative( JSON )Native调用 原生调用H5页面注册的方法,或者通知H5页面执行回调方法

如图

第二步:JS如何调用Native

在第一步中,我们定义好了全局桥对象,可以我们是通过它的callHandler方法来调用原生的,那么它内部经历了一个怎么样的过程呢?如下

callHandler函数内部实现过程

在执行callHandler时,内部经历了以下步骤:

(1)判断是否有回调函数,如果有,生成一个回调函数id,并将id和对应回调添加进入回调函数集合responseCallbacks中

(2)通过特定的参数转换方法,将传入的数据,方法名一起,拼接成一个url scheme

//url scheme的格式如

//基本有用信息就是后面的callbackId,handlerName与data

//原生捕获到这个scheme后会进行分析

var uri = CUSTOM_PROTOCOL_SCHEME://API_Name:callbackId/handlerName?data

(3)使用内部早就创建好的一个隐藏iframe来触发scheme

//创建隐藏iframe过程

var messagingIframe = document.createElement('iframe');

messagingIframe.style.display = 'none';

document.documentElement.appendChild(messagingIframe);

//触发scheme

messagingIframe.src = uri;

注意,正常来说是可以通过window.location.href达到发起网络请求的效果的,但是有一个很严重的问题,就是如果我们连续多次修改window.location.href的值,在Native层只能接收到最后一次请求,前面的请求都会被忽略掉。所以JS端发起网络请求的时候,需要使用iframe,这样就可以避免这个问题。---引自参考来源

第三步:Native如何得知api被调用

在上一步中,我们已经成功在H5页面中触发scheme,那么Native如何捕获scheme被触发呢?

根据系统不同,Android和iOS分别有自己的处理方式

Android捕获url scheme

在Android中(WebViewClient里),通过shouldoverrideurlloading可以捕获到url scheme的触发

public boolean shouldOverrideUrlLoading(WebView view, String url){

//读取到url后自行进行分析处理

//如果返回false,则WebView处理链接url,如果返回true,代表WebView根据程序来执行url

return true;

}

另外,Android中也可以不通过iframe.src来触发scheme,android中可以通过window.prompt(uri, "");来触发scheme,然后Native中通过重写WebViewClient的onJsPrompt来获取uri

iOS捕获url scheme

iOS中,UIWebView有个特性:在UIWebView内发起的所有网络请求,都可以通过delegate函数在Native层得到通知。这样,我们可以在webview中捕获url scheme的触发(原理是利用 shouldStartLoadWithRequest)

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

NSURL *url = [request URL];

NSString *requestString = [[request URL] absoluteString];

//获取利润url scheme后自行进行处理

之后Native捕获到了JS调用的url scheme,接下来就该到下一步分析url了

第四步:分析url-参数和回调的格式

在前面的步骤中,Native已经接收到了JS调用的方法,那么接下来,原生就应该按照定义好的数据格式来解析数据了

url scheme的格式 前面已经提到。Native接收到Url后,可以按照这种格式将回调参数id、api名、参数提取出来,然后按如下步骤进行

(1)根据api名,在本地找寻对应的api方法,并且记录该方法执行完后的回调函数id

(2)根据提取出来的参数,根据定义好的参数进行转化

如果是JSON格式需要手动转换,如果是String格式直接可以使用

(3)原生本地执行对应的api功能方法

(4)功能执行完毕后,找到这次api调用对应的回调函数id,然后连同需要传递的参数信息,组装成一个JSON格式的参数

回调的JSON格式为:{responseId:回调id,responseData:回调数据}

responseId String型 H5页面中对应需要执行的回调函数的id,在H5中生成url scheme时就已经产生

responseData JSON型 Native需要传递给H5的回调数据,是一个JSON格式: {code:(整型,调用是否成功,1成功,0失败),result:具体需要传递的结果信息,可以为任意类型,msg:一些其它信息,如调用错误时的错误信息}

(5)通过JSBridge通知H5页面回调

第五步:Native如何调用JS

到了这一步,就该Native通过JSBridge调用H5的JS方法或者通知H5进行回调了,具体如下

//将回调信息传给H5

JSBridge._handleMessageFromNative(messageJSON);

如上,实际上是通过JSBridge的_handleMessageFromNative传递数据给H5,其中的messageJSON数据格式根据两种不同的类型,有所区别,如下

Native通知H5页面进行回调

Native主动调用H5方法

Native主动调用H5方法时,数据格式是:{handlerName:api名,data:数据,callbackId:回调id}

handlerName String型 需要调用的,h5中开放的api的名称

data JSON型 需要传递的数据,固定为JSON格式(因为我们固定H5中注册的方法接收的第一个参数必须是JSON,第二个是回调函数)

callbackId String型 原生生成的回调函数id,h5执行完毕后通过url scheme通知原生api成功执行,并传递参数

注意,这一步中,如果Native调用的api是h5没有注册的,h5页面上会有对应的错误提示。

另外,H5调用Native时,Native处理完毕后一定要及时通知H5进行回调,要不然这个回调函数不会自动销毁,多了后会引发内存泄漏。

第六步:H5中api方法的注册以及格式

前面有提到Native主动调用H5中注册的api方法,那么h5中怎么注册供原生调用的api方法呢?格式又是什么呢?如下

H5中注册供原生调用的API

//注册一个测试函数

JSBridge.registerHandler('testH5Func',function(data,callback){

alert('测试函数接收到数据:'+JSON.stringify(data));

callback&&callback('测试回传数据...');

});

如上述代码为注册一个供原生调用的api

H5中注册的API格式注意

如上代码,注册的api参数是(data,callback)

其中第一个data即原生传过来的数据,第二个callback是内部封装过一次的,执行callback后会触发url scheme,通知原生获取回调信息

进一步完善JSBridge方案

在前文中,已经完成了一套JSBridge方案,这里,在介绍下如何完善这套方案

思路

github上有一个开源项目,它里面的JSBridge做法在iOS上进一步优化了,所以参考他的做法,这里进一步进行了完善。地址marcuswestin/WebViewJavascriptBridge

大致思路就是

h5调用Native的关键步骤进行拆分,由以前的直接传递url scheme变为传递一个统一的url scheme,然后Native主动获取传递的参数

完善以前: H5调用Native->将所有参数组装成为url scheme->原生捕获scheme,进行分析

完善以后: H5调用Native->将所有参数存入本地数组->触发一个固定的url scheme->原生捕获scheme->原生通过JSBridge主动获取参数->进行分析

实现

这种完善后的流程和以前有所区别,如下

JSBridge对象图解

JSBridge实现完整流程

注意

由于这次完善的核心是:Native主动调用JS函数,并获取返回值。而在Android4.4以前,Android是没有这个功能的,所以并不完全适用于Android

所以一般会进行一个兼容处理,Android中采用以前的scheme传法,iOS使用完善后的方案(也便于4.4普及后后续的完善)

完整的JSBridge

上述分析了JSBridge的实现流程,那么实际项目中,我们就应该结合上述两种,针对Android和iOS的不同情况,统一出一种完整的方案,如下

完整调用流程图

如上图,结合上述方案后有了一套统一JSBridge方案

另外实现:不采用url scheme方式

前面提到的JSBridge都是基于url scheme的,但其实如果不考虑Android4.2以下,iOS7以下,其实也可以用另一套方案的,如下

Native调用JS的方法不变

JS调用Native是不再通过触发url scheme,而是采用自带的交互,比如

Android中,原生通过 addJavascriptInterface开放一个统一的api给JS调用,然后将触发url scheme步骤变为调用这个api,其余步骤不变(相当于以前是url接收参数,现在变为api函数接收参数)

iOS中,原生通过JavaScriptCore里面的方法来注册一个统一api,其余和Android中一样(这里就不需要主动获取参数了,因为参数可以直接由这个函数统一接收)

当然了,这只是一种可行的方案,多一种选择而已,具体实现流程请参考前面系列文章,本文不再赘述

实现示例

示例说明

本文中包括两个示例,一个是基础版本的JSBridge实现,一个是完整版本的JSBridge实现(包括JS,Android,iOS实现等)

实现源码

基础版本的JSBridge

这里只介绍JS的实现,具体Android,iOS实现请参考完整版本,实现如下

(function() {

(function() {

var hasOwnProperty = Object.prototype.hasOwnProperty;

var JSBridge = window.JSBridge || (window.JSBridge = {});

//jsbridge协议定义的名称

var CUSTOM_PROTOCOL_SCHEME = 'CustomJSBridge';

//最外层的api名称

var API_Name = 'namespace_bridge';

//进行url scheme传值的iframe

var messagingIframe = document.createElement('iframe');

messagingIframe.style.display = 'none';

messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;

document.documentElement.appendChild(messagingIframe);

//定义的回调函数集合,在原生调用完对应的方法后,会执行对应的回调函数id

var responseCallbacks = {};

//唯一id,用来确保每一个回调函数的唯一性

var uniqueId = 1;

//本地注册的方法集合,原生只能调用本地注册的方法,否则会提示错误

var messageHandlers = {};

//实际暴露给原生调用的对象

var Inner = {

/**

* @description 注册本地JS方法通过JSBridge给原生调用

* 我们规定,原生必须通过JSBridge来调用H5的方法

* 注意,这里一般对本地函数有一些要求,要求第一个参数是data,第二个参数是callback

* @param {String} handlerName 方法名

* @param {Function} handler 对应的方法

*/

registerHandler: function(handlerName, handler) {

messageHandlers[handlerName] = handler;

},

/**

* @description 调用原生开放的方法

* @param {String} handlerName 方法名

* @param {JSON} data 参数

* @param {Function} callback 回调函数

*/

callHandler: function(handlerName, data, callback) {

//如果没有 data

if(arguments.length == 3 && typeof data == 'function') {

callback = data;

data = null;

}

_doSend({

handlerName: handlerName,

data: data

}, callback);

},

/**

* @description 原生调用H5页面注册的方法,或者调用回调方法

* @param {String} messageJSON 对应的方法的详情,需要手动转为json

*/

_handleMessageFromNative: function(messageJSON) {

setTimeout(_doDispatchMessageFromNative);

/**

* @description 处理原生过来的方法

*/

function _doDispatchMessageFromNative() {

var message;

try {

if(typeof messageJSON === 'string'){

message = JSON.parse(messageJSON);

}else{

message = messageJSON;

}

} catch(e) {

//TODO handle the exception

console.error("原生调用H5方法出错,传入参数错误");

return;

}

//回调函数

var responseCallback;

if(message.responseId) {

//这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseId

responseCallback = responseCallbacks[message.responseId];

if(!responseCallback) {

return;

}

//执行本地的回调函数

responseCallback(message.responseData);

delete responseCallbacks[message.responseId];

} else {

//否则,代表原生主动执行h5本地的函数

if(message.callbackId) {

//先判断是否需要本地H5执行回调函数

//如果需要本地函数执行回调通知原生,那么在本地注册回调函数,然后再调用原生

//回调数据有h5函数执行完毕后传入

var callbackResponseId = message.callbackId;

responseCallback = function(responseData) {

//默认是调用EJS api上面的函数

//然后接下来原生知道scheme被调用后主动获取这个信息

//所以原生这时候应该会进行判断,判断对于函数是否成功执行,并接收数据

//这时候通讯完毕(由于h5不会对回调添加回调,所以接下来没有通信了)

_doSend({

handlerName: message.handlerName,

responseId: callbackResponseId,

responseData: responseData

});

};

}

//从本地注册的函数中获取

var handler = messageHandlers[message.handlerName];

if(!handler) {

//本地没有注册这个函数

} else {

//执行本地函数,按照要求传入数据和回调

handler(message.data, responseCallback);

}

}

}

}

};

/**

* @description JS调用原生方法前,会先send到这里进行处理

* @param {JSON} message 调用的方法详情,包括方法名,参数

* @param {Function} responseCallback 调用完方法后的回调

*/

function _doSend(message, responseCallback) {

if(responseCallback) {

//取到一个唯一的callbackid

var callbackId = Util.getCallbackId();

//回调函数添加到集合中

responseCallbacks[callbackId] = responseCallback;

//方法的详情添加回调函数的关键标识

message['callbackId'] = callbackId;

}

//获取 触发方法的url scheme

var uri = Util.getUri(message);

//采用iframe跳转scheme的方法

messagingIframe.src = uri;

}

var Util = {

getCallbackId: function() {

//如果无法解析端口,可以换为Math.floor(Math.random() * (1 << 30));

return 'cb_' + (uniqueId++) + '_' + new Date().getTime();

},

//获取url scheme

//第二个参数是兼容android中的做法

//android中由于原生不能获取JS函数的返回值,所以得通过协议传输

getUri: function(message) {

var uri = CUSTOM_PROTOCOL_SCHEME + '://' + API_Name;

if(message) {

//回调id作为端口存在

var callbackId, method, params;

if(message.callbackId) {

//第一种:h5主动调用原生

callbackId = message.callbackId;

method = message.handlerName;

params = message.data;

} else if(message.responseId) {

//第二种:原生调用h5后,h5回调

//这种情况下需要原生自行分析传过去的port是否是它定义的回调

callbackId = message.responseId;

method = message.handlerName;

params = message.responseData;

}

//参数转为字符串

params = this.getParam(params);

//uri 补充

uri += ':' + callbackId + '/' + method + '?' + params;

}

return uri;

},

getParam: function(obj) {

if(obj && typeof obj === 'object') {

return JSON.stringify(obj);

} else {

return obj || '';

}

}

};

for(var key in Inner) {

if(!hasOwnProperty.call(JSBridge, key)) {

JSBridge[key] = Inner[key];

}

}

})();

//注册一个测试函数

JSBridge.registerHandler('testH5Func', function(data, callback) {

alert('测试函数接收到数据:' + JSON.stringify(data));

callback && callback('测试回传数据...');

});

/*

***************************API********************************************

* 开放给外界调用的api

* */

window.jsapi = {};

/**

***app 模块

* 一些特殊操作

*/

jsapi.app = {

/**

* @description 测试函数

*/

testNativeFunc: function() {

//调用一个测试函数

JSBridge.callHandler('testNativeFunc', {}, function(res) {

callback && callback(res);

});

}

};

})();

完整版本的JSBridge

jsbridge实现及原理_Hybrid APP基础篇(四)-JSBridge的原理相关推荐

  1. 自动驾驶-毫米波雷达系列基础篇-测角原理

    雷达的测角原理 3.1 单目标的测角原理 3.2 最大视场角 3.3 同距同速的多个目标 3.4 角度分辨率 3.5 对比角度估计和速度估计 当目标距离发生很小的变化时,会导致Range-FFT峰值处 ...

  2. Kafka核心设计与实践原理总结:基础篇

    作者:未完成交响曲,资深Java工程师!目前在某一线互联网公司任职,架构师社区合伙人! 一.基本概念 1.体系架构 Producer:生产者 Consumber:消费者 Broker:服务代理节点(k ...

  3. 玩转Luat 基础篇④——矩阵键盘的原理及应用

    文章目录 一.前言 二.工作原理 2.1 行扫描和列扫描 2.2 扫描的方式 三.快速应用 四.参考 一.前言 使用矩阵键盘的好处,在于节省IO口 使用矩阵键盘的坏处,在于需要行列扫描,增加CPU负担 ...

  4. 测试需要了解的技术之基础篇四__UI自动化测试体系

    UI自动化测试体系 1.Andriod 自动化测试:Appium 环境安装与架构介绍.Appium Desktop用例录制.Appium测试用例流程.元素定位方法 IA/AID/XPATH/UISel ...

  5. 计算机基础(四)——DNS原理,让开发常忘的a记录,ns记录和cname都是什么

    目录 一.为什么要熟悉DNS原理 二.dig追踪dns解析流程 1.开始从114.114.114.119服务器查询全球的根域名.root ns记录 服务器地址 2.最先返回的是jroot服务器,返回1 ...

  6. java程序试岗内容_java程序员修炼之路基础篇四:继承

    上一篇文章我跟大家聊了一下"封装",今天我们聊一下同样作为java语言三大特征之一的"继承". 简单说"继承"就是从一个已知类派生出新类的过 ...

  7. 前端开发之JavaScript基础篇四

    主要内容: 1.定时器 2.正则表达式入门 3.元字符 4.正则表达式实战运用 一.定时器 javaScript里主要使用两种定时器,分别是:setInterval()和setTimeout(). 1 ...

  8. 转载:谢谢原作者:块设备驱动实战基础篇四 (逐渐成型,加入ioctl通信机制)

    1.6介绍一种内核与用户空间通信的方法-misc设备ioctl机制 块设备驱动开发中往往需要配合用户态的管理程序工具,管理我们的块设备,此时我们需要涉及用户空间程序与块设备通信的方法,ioctl机制就 ...

  9. c if语句多个条件判断顺序_Java中的流程控制语句 (基础篇四)

    流程控制就是对事物次序的布置和安排,在程序中就是对代码执行次序的安排和控制 程序中的流程控制主要有三种:顺序流程.选择流程.循环流程. 顺序流程:比如打印输出的代码按照指定的顺序结构依次排序,打印的结 ...

  10. java中的四个跳转语句_Java中的流程控制语句 (基础篇四)

    流程控制就是对事物次序的布置和安排,在程序中就是对代码执行次序的安排和控制 程序中的流程控制主要有三种:顺序流程.选择流程.循环流程. 顺序流程:比如打印输出的代码按照指定的顺序结构依次排序,打印的结 ...

最新文章

  1. mysql 自定义提示符
  2. 2层,3层,4层交换机的区别与特点!!
  3. 替换空格---剑指Offer
  4. 互联网 4 大发明之二维码,你如何使用 Python 生成二维码?
  5. 支持python开发的环境有哪些变化_Python开发实践:打造完美的项目工程环境
  6. 十、Linux文件系统基本操作(mount挂载,umount卸载)
  7. HDUOJ 1428
  8. php用户注册重复_php如何禁止重复注册用户名
  9. [原创]Installshield工具介绍
  10. 定位CoreLocation 使用
  11. web技术基础---网站设计说明书
  12. canvas 角度 弧度 换算
  13. win2008服务器c盘在线扩容,win7 win10 win2008系统给主分区C盘增加空间 不破坏原硬盘内容扩充C盘 MiniTool Partition Wizard...
  14. qml创建无边框带阴影的模态窗口
  15. Mac清倒废纸篓提示“voicetrigger“在使用中
  16. 《具体数学》部分习题解答4
  17. 80老翁谈人生(198):老翁谈人生系列短文目录索引
  18. ElasticSearch创建索引:[hotel/6g9tufKRuWDdWfgE_F30Q] ElasticsearchStatusException[Elasticsearch exception
  19. Spark学习(1)-Spark基础
  20. 3d打印驱动开启uart有什么用TMC2208如何在Ramps1.4开启uart

热门文章

  1. 全网、全国最好用的基于经纬度的天气查询、天气预报接口
  2. windows xp共享访问提示“拒绝访问”的故障处理步骤
  3. 项目过程中的部分逻辑
  4. 判断当前页面是否在微信浏览器中打开
  5. vue项目中axios请求网络接口封装
  6. MySQL查询用户行为,网站用户行为分析
  7. css设置table阴影浮动效果
  8. Redis搭建及介绍
  9. Swoft单元测试基本坑
  10. uniapp uView u-picker组件三级联动Demo