Hybrid APP基础篇(四)->JSBridge的原理

说明

JSBridge实现原理

目录

  • 前言

    • 参考来源
    • 前置技术要求
    • 楔子
  • 原理概述
    • 简介
    • url scheme介绍
  • 实现流程
    • 实现思路
    • 第一步:设计出一个Native与JS交互的全局桥对象
    • 第二步:JS如何调用Native
    • 第三步:Native如何得知api被调用
    • 第四步:分析url-参数和回调的格式
    • 第五步:Native如何调用JS
    • 第六步:H5中api方法的注册以及格式
  • 进一步完善JSBridge方案
    • 思路
    • 实现
    • 注意
  • 完整的JSBridge
    • 完整调用流程图
    • 另外实现:不采用url scheme方式
  • 实现示例
    • 示例说明
    • 实现源码

前言

参考来源

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

前置技术要求

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

楔子

上文中简单的介绍了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如何调用JS

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

//将回调信息传给H5
JSBridge._handleMessageFromNative(messageJSON);
			

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

Native通知H5页面进行回调

数据格式为: Native通知H5回调的JSON格式

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

由于内容较多,已经单独提取成一个模块,参考 Hybrid APP基础篇(五)->JSBridge实现示例

时间: 2024-04-30 00:06:47

Hybrid APP基础篇(四)->JSBridge的原理的相关文章

Hybrid APP基础篇(五)-&gt;JSBridge实现示例

说明 JSBridge实现示例 目录 前言 参考来源 楔子 JS实现部分 说明 实现 Android实现部分 说明 JSBridge类 实现 Callback类 实现 Webview容器关键代码 实现 API 类实现 iOS实现部分 说明 WebViewJavascriptBridgeBase 实现 WebViewJavascriptBridge 实现 Webview容器关键代码 实现 前言 参考来源 前人栽树,后台乘凉,本文参考了以下来源 Hybrid APP架构设计思路 marcuswest

Hybrid APP基础篇(二)-&gt;Native、Hybrid、React Native、Web App方案的分析比较

说明 Native.Hybrid.React.Web App方案的分析比较 目录 前言 参考来源 前置技术要求 楔子 几种APP开发模式 概述 Native App Web App Hybrid App React Native App 分析 各大开发模式直观对比 如何选择开发模式 另类的app方案 微网页 微信小程序 其它 前言 参考来源 前人栽树,后台乘凉,本文参考了以下来源 对当前主流hybrid app.web app与native app工具的初步比较与分析 H5.React Nati

Hybrid APP基础篇(一)-&gt;什么是Hybrid App

说明 Hybrid APP是目前广泛流行的一种APP开发模式,本文对其做简单介绍 目录 前言 参考来源 楔子 Hybrid发家史 突然兴盛的H5 H5大行其道 H5渗入APP开发 Hybrid的兴盛 Hybrid概述 Hybrid定义 Hybrid App的类型划分 Hybrid架构 基本原理 内部的实现原理流程 Hybrid的未来 现行多种App开发模式以及分析比较 Hybrid面临的挑战 前言 参考来源 前人栽树,后台乘凉,本文参考了以下来源 HTML5 APP----2014年H5没火,w

JavaScript基础篇(四)— — 函数

一.函数基础 ??1.返回值:如果某个函数没有显式的return返回值,默认它的返回值为undefined ??2.参数:内建变量arguments,能返回函数所接收的所有参数 ???? ??3.预定义(内建)函数 -- isNaN: ????a.检测parseInt / parseFloat调用是否成功. ???? ????b.NaN不存在等值的概念, 也就是说表达式NaN === NaN 返回的是false 二.函数的变量作用域 ??1.变量提升:函数域优先于全局域,所有局部a会覆盖掉所有与

Java面试题-基础篇四

31.String s = new String("xyz");创建了几个StringObject?是否可以继承String类? 两个或一个都有可能,"xyz"对应一个对象,这个对象放在字符串常量缓冲区,常量"xyz"不管出现多少遍,都是缓冲区中的那一个.NewString每写一遍,就创建一个新的对象,它使用常量"xyz"对象的内容来创建出一个新String对象.如果以前就用过'xyz',那么这里就不会创建"xyz&

react基础篇四

列表 & Keys 渲染多个组件 你可以通过使用{}在JSX内构建一个元素集合 下面,我们使用Javascript中的map()方法遍历numbers数组.对数组中的每个元素返回<li>标签,最后我们得到一个数组listItems 我们把整个listItems插入到ul元素中,然后渲染进DOM: ReactDOM.render( <ul>{listItems}</ul>, document.getElementById('root') ); function N

Linux基础篇四———管道命令

管道命令 如果我们的数据必须经过"几道手续"之后才能够得到我们想要的格式那么我们必须使用pipe命令来解决这个问题 **管道命令和连续执行命令是不一样的 我们来看一下管道命令的处理信息流程 管道命令的应用 1.cut **用于选取部分数据 -d:规定分隔符 -f:依据-d分隔之后所选取的数据段 -c:规定以字符为单位分割接数字来取到置顶位置数据. **cut的主要用途在于将同一行数据进行分解 2.grep **用于分析一行数据 -a: 将二进制文件以text文件的方式查找数据 -c:计

传智的光辉岁月-C#基础篇四数组

using System; using System.Collections.Generic; using System.Linq; using System.Text; namespace P01Array { class Program { static void Main(string[] args) { int[] arr = new int[2] { 22, 33 }; double dd = arr[0]; ArrTurnBig(); Console.ReadLine(); } #r

Python基础篇(四)

Python中的字典类似于Java中的Map,数据以键值对的形式存储. 字典可以用以下的方式使用: >>> phonebook = {"alice":"6100","bruce":"6230","beth":"9120"}     >>> phonebook.get("alice")     '6100' 获取字典中某个键的值,可以