OC与JS的交互详解

事情的起因还是因为项目需求驱动。折腾了两天,由于之前没有UIWebView与JS交互的经历,并且觉得这次在功能上有一定的创造性,特此留下一点文字,方便日后回顾。

我要实现这样一个需求:按照本地的CSS文件展示一串网络获取的带HTML格式的只有body部分的文本,需要自己拼写完整的HTML。除此之外,还需要禁用获取的HTML文本中自带的 《 img 》 标签自动加载,并把下载图片的操作放在native端来处理,并通过JS将图片在Cache中的地址返回给UIWebview。

之所以要把图片操作放在native端做的好处在于:

  • 1、可以进行本地缓存,下次进入这篇文章可以直接从缓存读取,提高响应速度并且节省用户流量。
  • 2、可以实现点击图片放大、保存图片到相册等操作。

技术难点也有两个:

  • 1、如何让HTML文本onLoad的时候,禁用自身的图片加载而是从本地获取图片?
  • 2、如何把native端下载好的图片返回给网页?

起初,我也是束手无策,翻看文档可只找到了一个 - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script 和JS简易交互的方法,未能如愿。直到我在Github上看到了WebViewJavascriptBridge 这个用于UIWebView/WebViews和JS交互的封装库。

刚看sample的时候我差点没被各种回调搞晕,好记性不如烂笔头,我从来不掩饰自己的愚笨,所以我画了一个关系图。在放图之前,我们先看代码。

一开始,我们在Native端和JS端都分别进行初始化:

OC端:

1 @property WebViewJavascriptBridge* bridge;

对应的初始化代码如下,在初始化中直接包含了一个用于接收JS的回调:

1 _bridge = [WebViewJavascriptBridge bridgeForWebView:webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {
2         NSLog(@"ObjC received message from JS: %@", data);
3         responseCallback(@"Response for message from ObjC");
4 }];

JS端:(以下是固定写法,你自己的JS文件中必须包含如下代码)

 1 function connectWebViewJavascriptBridge(callback) {
 2     if (window.WebViewJavascriptBridge) {
 3         callback(WebViewJavascriptBridge)
 4     } else {
 5         document.addEventListener(‘WebViewJavascriptBridgeReady‘,   function() {
 6             callback(WebViewJavascriptBridge)
 7         }, false)
 8     }
 9 }
10 connectWebViewJavascriptBridge(function(bridge) {
11     bridge.init(function(message, responseCallback) {
12         log(‘JS got a message‘, message)
13         var data = { ‘Javascript Responds‘:‘Wee!‘ }
14         log(‘JS responding with‘, data)
15         responseCallback(data)
16     })
17 }

然后,我们要知道,在WebViewJavascriptBridge中,交互的方式只有两种:send 和 callHandle,JS和OC都有这两个方法,所以对应的四种关系是:

以上表中的对应关系的解读是,例如第一条:在JS中如果调用了bridge.send(),那么将触发OC端_bridge初始化方法中的回调。

同理,第二条,在JS中调用了bridge.callHandler(‘testJavascriptHandler‘),它将触发OC端注册的同名方法:

1 bridge.registerHandler(‘testJavascriptHandler‘, function(data, responseCallback) {
2         log(‘ObjC called testJavascriptHandler with‘, data)
3         var responseData = { ‘Javascript Says‘:‘Right back atcha!‘ }
4         log(‘JS responding with‘, responseData)
5         responseCallback(responseData)
6 })

了解了使用规则,下面来看看在我们这个实际需求中应用的整体思路:

—— 1 ——

首先,我们要做的第一步是替换获取的HTML文本中默认的src,以避免其会自动加载图片。

1 NSString *_content = [contentstring stringByReplacingOccurrencesOfString:@"src" withString:@"esrc"];

—— 2 ——

因为我们获取的只是HTML的body部分,因此我们需要自己书写完整的HTML。

我们让《body 》的时候去调用JS中的 onLoaded()函数。在这个函数中我们遍历所有img标签的 esrc,保存为一个数组返回给 OC 端,让native端去下载这些图片。

 1 function onLoaded() {
 2     connectWebViewJavascriptBridge(function(bridge) {
 3         var allImage = document.querySelectorAll("img");
 4         allImage = Array.prototype.slice.call(allImage, 0);
 5         var imageUrlsArray = new Array();
 6         allImage.forEach(function(image) {
 7             var esrc = image.getAttribute("esrc");
 8             var newLength = imageUrlsArray.push(esrc);
 9         });
10         bridge.send(imageUrlsArray);
11     });
12 }

—— 3 ——

bridge.send 会触发WebViewJavascriptBridge初始化方法 + (instancetype)bridgeForWebView:(WVJB_WEBVIEW_TYPE*)webView webViewDelegate:(WVJB_WEBVIEW_DELEGATE_TYPE*)webViewDelegate handler:(WVJBHandler)handler; 中的handler,我们在handler的block中下载所有图片。并且把下载完的图片在cache中的地址返回个JS。

 1 #pragma mark -- 下载全部图片
 2 -(void)downloadAllImagesInNative:(NSArray *)imageUrls{
 3     SDWebImageManager *manager = [SDWebImageManager sharedManager];
 4     //初始化一个置空元素数组
 5     _allImagesOfThisArticle = [NSMutableArray arrayWithCapacity:imageUrls.count];//本地的一个用于保存所有图片的数组
 6     for (NSUInteger i = 0; i < imageUrls.count-1; i++) {
 7         [_allImagesOfThisArticle addObject:[NSNull null]];
 8     }
 9     for (NSUInteger i = 0; i < imageUrls.count-1; i++) {
10         NSString *_url = imageUrls[i];
11         [manager downloadImageWithURL:[NSURL URLWithString:_url] options:SDWebImageHighPriority progress:nil completed:^(UIImage *image, NSError *error, SDImageCacheType cacheType, BOOL finished, NSURL *imageURL) {
12             if (image) {
13     dispatch_async(dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_DEFAULT, 0), ^{
14                     NSString *imgB64 = [UIImageJPEGRepresentation(image, 1.0) base64EncodedStringWithOptions:NSDataBase64Encoding64CharacterLineLength];
15                     //把图片在磁盘中的地址传回给JS
16                     NSString *key = [manager cacheKeyForURL:imageURL];
17                     NSString *source = [NSString stringWithFormat:@"data:image/png;base64,%@", imgB64];
18                     [_bridge callHandler:@"imagesDownloadComplete" data:@[key,source]];
19                 });
20             }
21         }];
22     }
23 }

—— 4 ——

[_bridge callHandler:@"imagesDownloadComplete" data:@[key,source]] 会触发JS中的 function imagesDownloadComplete()。在这个函数中遍历所有img标签,把传过来的图片地址赋值给img的src。

1 function imagesDownloadComplete(pOldUrl, pNewUrl) {
2     var allImage = document.querySelectorAll("img");
3     allImage = Array.prototype.slice.call(allImage, 0);
4     allImage.forEach(function(image) {
5         if (image.getAttribute("esrc") == pOldUrl || image.getAttribute("esrc") == decodeURIComponent(pOldUrl)) {
6             image.src = pNewUrl;
7         }
8     });
9 }

至此,通过WebViewJavascriptBridge处理UIWebView和JS交互实现本地处理网页图片的下载操作就基本完成了。这个例子展现了一个完整的过程,基本涉及了JS和OC的各种交互包括OC调用JS、JS调用OC等。如果你有其它的业务需求,也基本按照这个流程就可以依样画葫芦了,唯一不同的也就是业务逻辑了。

下面我再举一个例子。也是出现在我的业务需求里的,就是点击网页上的图片,图片会以Zoom-out的动画放大,左右滑动可以查看其它图片,同时还需要双击放大查看、保存图片等功能。 类似这样:

乍一看,我们点击的是一张网页上的图片,怎么可能让这张图片单独跳出来?而且还能左右滑动显示其它图片?

首先我们还是需要去改造网络获取的那段HTML文本,正则匹配出 img esrc=http://....,加上onClick事件,绑定一个JS的方法,并把这个esrc作为参数传入这个绑定的方法中。

//正则替换

NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(《img[^》]+esrc=\")(\\S+)\"" options:0 error:nil];

result = [regex stringByReplacingMatchesInString:newContent options:0 range:NSMakeRange(0, newContent.length) withTemplate:@"《img esrc=\"$2\" onClick=\"javascript:onImageClick(‘$2‘)\""];

JS中onImageClick()函数。这个函数的主要任务是:获取点击图片的在所有图片中的编号以及在当前屏幕中的位置。并把这些信息返回给OC。

 1 function onImageClick(picUrl){
 2     connectWebViewJavascriptBridge(function(bridge) {
 3         var allImage = document.querySelectorAll("p img[esrc]");
 4         allImage = Array.prototype.slice.call(allImage, 0);
 5         var urls = new Array();
 6         var index = -1;
 7         var x = 0;
 8         var y = 0;
 9         var width = 0;
10         var height = 0;
11         allImage.forEach(function(image) {
12             var imgUrl = image.getAttribute("esrc");
13             var newLength = urls.push(imgUrl);
14             if(imgUrl == picUrl || imgUrl == decodeURIComponent(picUrl)){
15                 index = newLength-1;
16                 x = image.getBoundingClientRect().left;
17                 y = image.getBoundingClientRect().top;
18                 x = x + document.documentElement.scrollLeft;
19                 y = y + document.documentElement.scrollTop;
20                 width = image.width;
21                 height = image.height;
22                 console.log("x:"+x +";y:" + y+";width:"+image.width +";height:"+image.height);
23             }
24         });
25         console.log("检测到点击");
26         bridge.callHandler(‘imageDidClicked‘, {‘index‘:index,‘x‘:x,‘y‘:y,‘width‘:width,‘height‘:height}, function(response) {
27             console.log("JS已经发出imgurl和index,同时收到回调,说明OC已经收到数据");
28         });
29     });
30 }

bridge.callHandler 会触发OC中的 [_bridge registerHandler:@"imageDidClicked" handler:^(id data, WVJBResponseCallback responseCallback){}]。我们可以再handler中获得JS传过来的点击图片在所有图片中的编号,以及点击图片在当前图片中的空间位置。要实现点击图片Zoom-out的效果,我们要善于「作弊」。网页中的图片固然不能「跳」出来放大,但我们可以根据JS传回来的x、y、width、height这些位置信息自己创建一个UIImageView,image和当前点击图片一致,设置透明度为0,add到UIWebView上面。并通过IDMPhotoBrowser 这个开源库实现图片浏览。

 1   [_bridge registerHandler:@"imageDidClicked" handler:^(id data, WVJBResponseCallback responseCallback) {
 2         NSInteger index = [[data objectForKey:@"index"] integerValue];
 3         CGFloat originX = [[data objectForKey:@"x"] floatValue];
 4         CGFloat originY = [[data objectForKey:@"y"] floatValue];
 5         CGFloat width   = [[data objectForKey:@"width"] floatValue];
 6         CGFloat height  = [[data objectForKey:@"height"] floatValue];
 7         tappedImageView.alpha = 0;
 8         tappedImageView.frame = CGRectMake(originX, originY, width, height);
 9         tappedImageView.image = _allImagesOfThisArticle[index];//_allImagesOfThisArticle是一个本地数组用来存放所有图片
10         NSLog(@"OC已经收到JS的imageDidClicked了: %@", data);
11         responseCallback(@"OC已经收到JS的imageDidClicked了");
12         //点击放大图片
13         [self presentPhotosBrowserWithInitialPage:index animatedFromView:tappedImageView];
14     }];

Tips

由于我用的是Sublime Text,所以无法进行JS的调试。如果要用Atom调试,又感觉有点小题大做。我就是想要有个地方可以轻松地看到是否打印出了console.log或者JS函数是否被调用了。始终相信,任何问题都是可以解决的。我们可以用Safari。

连上你的iPhone或者使用模拟器,当你的程序当前显示了一个UIWebView,Safari会自动识别这个UIWebview,并可以在开发菜单栏中找到你的设备进行调试。

选择控制台,你就可以看到久违的调试窗口以及JS的console.log了。

以上就是使用 WebViewJavascriptBridge 进行UIWebView与JS的深度交互的例子。

折腾WebViewJavascriptBridge的这几天下来,我最大的感受就是,学无止境。想想Node.JS都可以写服务器了,React Native都可以开发iOS了,而且Github上最流行的语言就是JS,导致我又有点想学JS了。但我又冷静了下来,明确自己的主要任务什么,想到Swift都没吃透呢,哪有时间去搞前端。根据我的经验,让我睡一觉,估计明天就没这个想法了^-^

时间: 2024-10-08 02:32:04

OC与JS的交互详解的相关文章

[转]OC与JS的交互详解

http://www.cnblogs.com/wenxp2006/p/4777937.html 事情的起因还是因为项目需求驱动.折腾了两天,由于之前没有UIWebView与JS交互的经历,并且觉得这次在功能上有一定的创造性,特此留下一点文字,方便日后回顾. 我要实现这样一个需求:按照本地的CSS文件展示一串网络获取的带HTML格式的只有body部分的文本,需要自己拼写完整的HTML.除此之外,还需要禁用获取的HTML文本中自带的 < img > 标签自动加载,并把下载图片的操作放在native

实现OC与JS的交互

oc-->js  stringByEvaluatingJavaScriptFromString,其参数是一NSString 字符串内容是js代码(这又可以是一个js函数.一句js代码或他们的组合),当js函数有返回值或一句js代码有值返回可通过stringByEvaluatingJavaScriptFromString的返回值获取. js-->oc 利用webView的重定向原理(即重新在js中指定document.location的值,此为一url),只要在这个url字符串中按自定义的规则指

js new date详解

创建一个日期对象: var objDate=new Date([arguments list]); var ini_date=new Date(2014,7,0); //是代表7月最后一天 ,也就是2014-07-31 var ini_date=new Date(2014,7,1); //是代表8月第一天 ,也就是2014-08-01参数形式有 以下5种: new Date("month dd,yyyy hh:mm:ss");   new  Date("month dd,yy

oc中字典的应用详解

NSDictionary *dic=[NSDictionary dictionaryWithObject:@"卢灿小样" forKey:@"lucan"]; NSLog(@"%@",dic); NSLog(@"%@",[dic objectForKey:@"lucan"]); //输出dic键值对个数 NSLog(@"%d",dic.count); NSDictionary *dic1=

【JS】☆★之详解[Object HTMLDivElement]和[Object Object]

[JS]☆★之详解[Object HTMLDivElement]和[Object Object] <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> <html xmlns="http://www.w3.org/1999/xhtml">

OC与JS的交互

关于oc与js的交互,本人用的是原生类 JSExport 实现. 本人不擅长文字描述,故直接上代码 1,首先创建一个继承 NSObject的类,创建代理,实现代理方法,.h文件 1 #import <Foundation/Foundation.h> 2 #import <JavaScriptCore/JavaScriptCore.h> 3 @protocol JSObjcDelegate1 <JSExport> 4 - (void)getUser:(NSString *

Js apply 方法 详解

Js apply方法详解 我在一开始看到JavaScript的函数apply和call时,非常的模糊,看也看不懂,最近在网上看到一些文章对apply方法和call的一些示例,总算是看的有点眉目了,在这里我做如下笔记,希望和大家分享..  如有什么不对的或者说法不明确的地方希望读者多多提一些意见,以便共同提高.. 主要我是要解决一下几个问题: 1.        apply和call的区别在哪里 2.        什么情况下用apply,什么情况下用call 3.        apply的其他

JS事件类型详解

一般事件 onclick IE3.N2 鼠标点击时触发 此事件 ondblclick IE4.N4 鼠标双击时触发 此事件 onmousedown IE4.N4 按下鼠标时触发 此事件 onmouseup IE4.N4 鼠标按下后松开鼠标时触发 此事件 onmouseover IE3.N2 当鼠标移动到某对象范围的上方时触发 此事件 onmousemove IE4.N4 鼠标移动时触发 此事件 onmouseout IE4.N3 当鼠标离开某对象范围时触发 此事件 onkeypress IE4.

Node.js继承中的静态类对象(《node.js开发实战详解》书中一些错误的改正)

今天气真好,最近挂掉一些面试之后心情略失落. 神马都是浮云,要永远做好世界第二. 不多提了,你问我心态为啥变好了.-------都是情怀,,. 嗯啊,最近在研究node. 别人问?你这水平还node... 哈哈哈,好伤心.... 不多提了,言归正传. 神马模块化神马的先就不多讲了,就一个module.export和export区别,后者对象的属性属于前者,逆命题不成立. 还有util.inherits(A,B)这个API注意一下A只会继承B的原型方法,原型以外的不会继承.不是说原型中数据是共享的