IOS、Android与H5通信-JsBridge原理(总结)
H5和原生app(ios,android)交互的载体基本都是基于Webview,可以把Webview看作是一个性能打八折的移动浏览器。
ios调用Javascript
简单说下这几种:WKWebView 、UIWebView、JavaScriptCore
WKWebView:苹果在ios8之后也引入了专门负责处理网页视图的框架WebKit,Webkit是啥,接触过H5、chrome的肯定都知道。chrome使用的也是基于webkit内核的Chromium引擎。WKWebView优点很多,支持更多H5特性,刷新效率及内置手势等,更加强大,性能也更优,不一一列举,如果大家app不需要兼容7及以下版本,不需要拦截一些请求,直接解析本地一些文件,建议使用WKWebView。
UIWebView:较老webview,第一代。其中stringByEvaluatingJavaScriptFromString方法提供了OC与js交互的能力。
JavaScriptCore(ios7及以后版本)。JavaScriptCore框架是webkit重要组成部分,主要是对JS进行解析和提供执行环境,Javascript的虚拟机,有点类似v8引擎,我自己这么理解:)正是它为ios提供了执行JavaScript代码的能力。ReactNative应该都是通过JavaScriptCore去解析的(自己猜测)。
微信小程序的逻辑层也是由JavaScriptCore作为运行环境。
Javascript 调用 ios(oc、swift)原理:
目前兼顾兼容性、比较成熟的方案还是通过拦截URL的方式。
UIWebView的特性,在UIWebView内发起的所有网络请求,都可以在Native层被捕捉到。
利用这一特性,就可以在UIWebView内发起一个自定义的网络请求,一般格式:jsbridge://method?参数1=value1&参数2=value2
于是在UIWebView中,只要发现是jsbridge://开头的url,就不进行内容的加载,而是执行相应的逻辑处理。
嵌入webview的h5中的js一般是通过动态创建隐藏iframe标签,赋值上文提到的链接给src,iframe不会引起页面调转、刷新。
主要代码:
var src= \'jsbridge://method?参数1=value1&参数2=value2\';var iframe = document.createElement(\'iframe\');iframe.style.display = \'none\';iframe.src = src;document.body.appendChild(iframe);//再删除iframesetTimeout(function() {iframe.remove();}, 50);
Android和Javascript互相调用
Android WebView也是基于WebKit引擎的一个组件,Android的Webview在低版本和高版本采用了不同的webkit版本内核,4.4后直接使用了Chrome。
这个组件功能非常强大,除了具有一般View的属性和设置外,还可以对url请求、页面加载、渲染、页面交互进行强大的处理。
Android调用JS代码的方法主要有2种:
1、WebView的loadUrl
2、WebView的evaluateJavascript
JS调用Android代码的方法主要有3种:
1、WebView的addJavascriptInterface进行对象映射(低版本Android4以下好像有一些安全问题,本人没有验证)
2、WebViewClient 的 shouldOverrideUrlLoading 方法回调拦截 url
3、WebChromeClient 的onJsAlert、onJsConfirm、onJsPrompt方法回调拦截JS对话框alert()、confirm()、prompt() 消息
一般常用onJsPrompt、prompt进行回调拦截
JSBridge是Native代码与JS代码的通信桥梁
设计一个jsbridge主要分几大步骤:
第一步:设计出一个Native与JS交互的全局中间对象
第二步:JS如何调用Native
第三步:Native如何得知api被调用
第四步:分析url-参数和回调的格式
第五步:Native如何调用JS
第六步:H5中api方法的注册以及格式
H5端JS核心代码:
(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传值的iframevar messagingIframe = document.createElement(\'iframe\');messagingIframe.style.display = \'none\';messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + \'://\' + API_Name;document.documentElement.appendChild(messagingIframe);//定义的回调函数集合,在原生调用完对应的方法后,会执行对应的回调函数idvar responseCallbacks = {};//唯一id,用来确保每一个回调函数的唯一性var uniqueId = 1;//本地注册的方法集合,原生只能调用本地注册的方法,否则会提示错误var messageHandlers = {};//当原生调用H5注册的方法时,通过回调来调用(也就是变为了异步执行,加强安全性)var dispatchMessagesWithTimeoutSafety = true;//本地运行中的方法队列var sendMessageQueue = [];//实际暴露给原生调用的对象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) {//如果没有 dataif(arguments.length == 3 && typeof data == \'function\') {callback = data;data = null;}_doSend({handlerName: handlerName,data: data}, callback);},/*** iOS专用* @description 当本地调用了callHandler之后,实际是调用了通用的scheme,通知原生* 然后原生通过调用这个方法来获知当前正在调用的方法队列*/_fetchQueue: function() {var messageQueueString = JSON.stringify(sendMessageQueue);sendMessageQueue = [];return messageQueueString;},/*** @description 原生调用H5页面注册的方法,或者调用回调方法* @param {String} messageJSON 对应的方法的详情,需要手动转为json*/_handleMessageFromNative: function(messageJSON) {setTimeout(_doDispatchMessageFromNative);/*** @description 处理原生过来的方法*/function _doDispatchMessageFromNative() {var message;try {message = JSON.parse(messageJSON);} catch(e) {//TODO handle the exceptionconsole.error(\"原生调用H5方法出错,传入参数错误\");return;}//回调函数var responseCallback;if(message.responseId) {//这里规定,原生执行方法完毕后准备通知h5执行回调时,回调函数id是responseIdresponseCallback = 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) {//取到一个唯一的callbackidvar callbackId = Util.getCallbackId();//回调函数添加到集合中responseCallbacks[callbackId] = responseCallback;//方法的详情添加回调函数的关键标识message[\'callbackId\'] = callbackId;}var uri;//android中,可以通过onJsPrompt或者截取Url访问都行var ua = navigator.userAgent;if(ua.match(/(iPhone\\sOS)\\s([\\d_]+)/)||ua.match(/(iPad).*OS\\s([\\d_]+)/)) {//ios中,通过截取客户端url访问//因为ios可以不暴露scheme,而是由原生手动获取//正在调用的方法详情添加进入消息队列中,原生会主动获取sendMessageQueue.push(message);uri = Util.getUri();}else{//android中兼容处理,将所有的参数一起拼接到url中uri = Util.getUri(message);}//获取 触发方法的url scheme//采用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);});}};})();