objC与js通信实现--WebViewJavascriptBridge
场景
在移动端开发中,最为流行的开发模式就是hybmid开发,在这种native和h5的杂糅下,既能在某些需求中保证足够的性能,也可以在某些列表详情的需求下采用h5的样式控制来丰富内容。但是在大型产品的开发中,往往前端的职责不仅仅是h5的编写,还包括基本业务逻辑的实现,比如在h5页面中确定当前用户所在的城市(location),我们可以采用html5规范的Geolocation接口,但是更为通俗的做法是调用native的本地接口,因此这种常规的场景就涉及到了js和native层通信的问题,这在手淘开发中经常遇到,手淘提供了中间层windvane(jsBridge)来完成通信,不过由于windvane特殊性并未开源,因此本文着重分析WebViewJavascriptBridge框架(针对iOS)的通信机制。
突破口
iOS下h5页面承载在webView视图中,webView提供比较特殊的接口是stringByEvaluatingJavaScriptFromString方法,它让js字符串在当前的webview提供的js全局上下文中执行脚本,因此我们通过在objC层调用stringByEvaluatingJavaScriptFromString,执行h5下js得相关函数,以返回值的形式获取js端提供的相关调用函数数组并在webview下的上下文中执行函数数组,最终完成objC->js的通信(调用)。
js调用objC则有些特殊,不过依然利用stringByEvaluatingJavaScriptFromString方法实现基本通信,并在objC层针对webviewDelegate接口提供的webView:shouldStartLoadWithRequest:navigationType方法进行捕获js层的调用。具体的方法是通过创建一个隐藏的iframe并对其src按照既定的schema格式赋值,并在objC层的webView:shouldStartLoadWithRequest:navigationType判断schema是否正确,如正确,则加载执行相关脚本,否则不执行。
源码分析
objC层调用js层注册函数:
- (void)sendData:(id)data responseCallback:(WVJBResponseCallback)responseCallback handlerName:(NSString*)handlerName {
NSMutableDictionary* message = [NSMutableDictionary dictionary];
if (data) {
message[@"data"] = data;
}
if (responseCallback) {
NSString* callbackId = [NSString stringWithFormat:@"objc_cb_%ld", ++_uniqueId];
self.responseCallbacks[callbackId] = [responseCallback copy];
message[@"callbackId"] = callbackId;
}
if (handlerName) {
message[@"handlerName"] = handlerName;
}
[self _queueMessage:message];
}
参数data为传递给js层函数的参数,可为NSString、NSDictionary等对象,responseCallback则为objC层的回调,此回调函数执行流程简述为“js层注册函数执行完毕后,会返回带有responseId的消息,最后在objC层取出(回调存储在responseCallbacks字典中)对应的回调(即此处的responseCallback),并执行”,handlerName则为js层定义的函数名称。
源码中在_queueMessage方法进行逻辑判断:若在h5页面或者js资源并未加载完毕时,在objC层webview中就调用了js函数,则会把相关的操作(存储为Message格式)存储在startupMessageQueue,等待相关资源加载完毕(即在webview的webViewDidFinishLoad生命周期函数中执行存储在startupMessageQueue的命令数组,执行完毕并清空该队列)再调用js层函数;否则若startupMessageQueue队列为空,则直接执行暴露在js端的webViewJavascriptBridge._handleMessageFromObjC函数,获取被调用的函数名和传入参数,以及在objC层sendData:responseCallback:handlerName中设置的回调函数id--callbackId,最终执行js层注册函数,并最终向objC层发送“_doSend({ responseId:callbackResponseId, responseData:responseData })”格式的消息,待objC接收到消息,解析responseId,执行回调函数。
// 在objC端调用,用作ref
function _dispatchMessageFromObjC(messageJSON) {
setTimeout(function _timeoutDispatchMessageFromObjC() {
var message = JSON.parse(messageJSON)
var messageHandler
var responseCallback
// js调用objC成功后,objC返回带有responseId的消息,在js端执行回调
if (message.responseId) {
responseCallback = responseCallbacks[message.responseId]
if (!responseCallback) { return; }
responseCallback(message.responseData)
delete responseCallbacks[message.responseId]
} else {
// objC调用js后,构造返回给objC的消息
if (message.callbackId) {
var callbackResponseId = message.callbackId
// 执行回调成功后,选择性返回给objC
responseCallback = function(responseData) {
_doSend({ responseId:callbackResponseId, responseData:responseData })
}
}
var handler = WebViewJavascriptBridge._messageHandler
if (message.handlerName) {
handler = messageHandlers[message.handlerName]
}
try {
handler(message.data, responseCallback)
} catch(exception) {
if (typeof console != 'undefined') {
console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception)
}
}
}
})
js触发objC调用是从_doSend函数开始的,主要就是通过给iframe设置schema完成:
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
var QUEUE_HAS_MESSAGE = '__WVJB_QUEUE_MESSAGE__'
// js调用objC,通过对webview设置schema拦截
// 拦截成功后,通过在objC端evaluateJs的_fetchQueue()来获取传入的参数
function _doSend(message, responseCallback) {
if (responseCallback) {
var callbackId = 'cb_'+(uniqueId++)+'_'+new Date().getTime()
responseCallbacks[callbackId] = responseCallback
message['callbackId'] = callbackId
}
sendMessageQueue.push(message)
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
}
由此可见,schema的格式为“wvjbscheme://WVJB_QUEUE_MESSAGE”,objC在webview的代理方法中webView:shouldStartLoadWithRequest:navigationType侦听schema格式,进而判断是否执行js的命令。
js层调用objC层注册函数
正如上节提到,在webView:shouldStartLoadWithRequest:navigationType中侦听schema格式,判断是否消息是否来自js层的函数调用:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType: (UIWebViewNavigationType)navigationType {
if (webView != _webView) { return YES; }
NSURL *url = [request URL];
__strong WVJB_WEBVIEW_DELEGATE_TYPE* strongDelegate = _webViewDelegate;
if ([_base isCorrectProcotocolScheme:url]) {
if ([_base isCorrectHost:url]) {
NSString *messageQueueString = [self _evaluateJavascript:@"WebViewJavascriptBridge._fetchQueue();"];
[_base flushMessageQueue:messageQueueString];
} else {
[_base logUnkownMessage:url];
}
return NO;
} else if (strongDelegate && [strongDelegate respondsToSelector:@selector(webView:shouldStartLoadWithRequest:navigationType:)]) {
return [strongDelegate webView:webView shouldStartLoadWithRequest:request navigationType:navigationType];
} else {
return YES;
}
}
若消息来自于js层,则在webview上下文执行WebViewJavascriptBridge._fetchQueue()函数,该函数的定义为
function _fetchQueue() {
var messageQueueString = JSON.stringify(sendMessageQueue)
sendMessageQueue = []
return messageQueueString
}
_fetchQueue返回js端的调用命令队列messageQueueString,并在objC层执行flushMessageQueue:messageQueueString方法,将调用命令数组序列化,并执行objC层定义的函数,这个调用的过程类似上节中objC调用js层定义的函数,也是当objC层定义的函数执行完后,向js层触发消息,消息格式依然和上节“js向objC发送的消息格式一样,形如{ responseId:callbackResponseId, responseData:responseData }”,当js层接收到消息,执行js层的回调函数。
因此综上来看,不管objC和js如何通信,最为关键的就是stringByEvaluatingJavaScriptFromString方法,它构建起了objC和js通信的基石,“objC可以直接通过该方法调用js函数,js也可让objC通过该方法获取js的调用队列,从而在objC层执行命令”。
总结
上文提到的仅仅是大体的通信机制,具体的实现细节仍有很多需要注意,比如如何在js端侦听通信组件的初始化事件、应该在何时在objC层调用js定义的函数、objC发送消息中序列化特殊字符等等,但是通信的机制可以通过本文略知一二。
objC与js通信实现--WebViewJavascriptBridge的更多相关文章
- 父窗口,子窗口之间的JS"通信"方法
今天需要在iframe内做一个弹窗,但使用弹窗组件的为子窗口,所以弹窗只在子窗口中显示掩膜层和定位,这样不符合需求. 后来晓勇哥指点,了解到一个以前一直没关注到的东西,每个窗口的全局变量,其实都存在对 ...
- [ActionScript] AS3利用SWFObject与JS通信
首先介绍SWFObject的用法: swfobject.embedSWF(swfUrl, id, width, height, version, expressInstallSwfurl, flash ...
- flex与js通信、在浏览器中打开新窗口
一.flex与js通信(通过flex调用js方法) var urlR:URLRequest = new URLRequest("javascript:test('from flex')&qu ...
- [ActionScript3.0] AS3利用ExternalInterface与js通信
AS3代码,可做文档类; package { import flash.display.Sprite; import flash.events.*; import flash.external.Ext ...
- window之间、iframe之间的JS通信
一.Window之间JS通信 在开发项目过程中,由于要引入第三方在线编辑器,所以需要另外一个窗口(window),而且要求打开的window要与原来的窗口进行js通信,那么如何实现呢? 1.在原窗口创 ...
- OC与JS交互之WebViewJavascriptBridge
上一篇文章介绍了通过UIWebView实现了OC与JS交互的可能性及实现的原理,并且简单的实现了一个小的示例DEMO,当然也有一部分遗留问题,使用原生实现过程比较繁琐,代码难以维护.这篇文章主要介绍下 ...
- js 通信
js 页面间的通信 看了一下公司原来的代码,原页面ajax post返回一个页面完整的HTML,然后再打开一个新页面并输出ajax返回的所有代码到新页面上,在新页面上以表单提交的形式实现重定向. 任凭 ...
- chrome插件开发.在content_script异步加载页面后, 如何进行JS通信与调用的问题
使用场景 在开发Chrome插件时, 有一种需求: 要求在WEB页面显示一个浮动窗口(A), 在此窗口中允许用Ajax方式调用另一个服务器上的一个页面(B) B页面上有独立的功能用JS写functio ...
- iframe与父页面的js通信
1.父页面调用iframe中的函数: document.getElementById('myframe').contentWidow.fun1(); 2.在iframe中调用父页面中的函数: wind ...
随机推荐
- 奇异值分解(SVD)原理与在降维中的应用
奇异值分解(Singular Value Decomposition,以下简称SVD)是在机器学习领域广泛应用的算法,它不光可以用于降维算法中的特征分解,还可以用于推荐系统,以及自然语言处理等领域.是 ...
- 移动端IOS点击事件失效解决方案
解决方案 解决办法有 4 种可供选择: 1 将 click 事件直接绑定到目标元素(即 .target)上 2 将目标元素换成 <a> 或者 button 等可点击的元素 3 将 clic ...
- CSS HTML元素布局及Display属性
本篇文章主要介绍HTML的内联元素.块级元素的分类与布局,以及dispaly属性对布局的影响. 目录 1. HTML 元素分类:介绍内联元素.块级元素的分类. 2. HTML 元素布局:介绍内联元素. ...
- Ubuntu 16.10 安装KolourPaint 4画图工具
KolourPaint 4画图工具简单实用,可以绘画.视频处理和图标编辑: • 绘画:绘制图表和“手绘” • 视频处理:编辑截图和照片;应用特效 • 图标编辑:绘画剪贴和标识透明化 1.在Ubuntu ...
- 【SSM框架】Spring + Springmvc + Mybatis 基本框架搭建集成教程
本文将讲解SSM框架的基本搭建集成,并有一个简单demo案例 说明:1.本文暂未使用maven集成,jar包需要手动导入. 2.本文为基础教程,大神切勿见笑. 3.如果对您学习有帮助,欢迎各种转载,注 ...
- 对Thoughtworks的有趣笔试题实践
记得2014年在网上看到Thoughtworks的一道笔试题,当时觉得挺有意思,但是没动手去写.这几天又在网上看到了,于是我抽了一点时间写了下,我把程序运行的结果跟网上的答案对了一下,应该是对的,但是 ...
- 用神奇的currentColor制作简洁的颜色动画效果
先上一个兼容性总结图:老版本ie可以直接用复杂方法了,套用某表情包的话: 2016年了,做前端你还考虑兼容IE6?你这简直是自暴自弃! 好了,知道了兼容性,我们可以放心的使用了. 在CSS3中扩展了 ...
- 多线程同步工具——Lock
本文原创,转载请注明出处. 参考文章: <"JUC锁"03之 公平锁(一)> <"JUC锁"03之 公平锁(二)> 锁分独占锁与共享锁, ...
- 张小龙宣布微信小程序1月9日发布,并回答了大家最关心的8个问题
2016 年 12 月 28 日,张小龙在微信公开课 PRO 版的会场上,宣布了微信小程序的正式发布时间. 微信小程序将于 2017 年 1 月 9 号正式上线. 同时他解释称,小程序就像PC时代的网 ...
- IOS开发基础知识--碎片51
1:https关闭证书跟域名的验证 AFSecurityPolicy *securityPolicy = [AFSecurityPolicy defaultPolicy]; securityPolic ...