• UIWebView与JS的深度交互


    sculpture-356115_640.jpg

    事情的起因还是因为项目需求驱动。折腾了两天,由于之前没有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
    2
    3
    4
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {  
            NSLog(@"ObjC received message from JS: %@", data);
            responseCallback(@"Response for message from ObjC");
    }];

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

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

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

    blob.png

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

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

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

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

    blob.png

    —— 1 ——

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

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

    —— 2 ——

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

    blob.png

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

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

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

    —— 4 ——

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

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

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

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

    objs.gif

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

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

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

    Tips

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

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

    blob.png

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

    blob.png

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

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

  • 相关阅读:
    leetcode 104. Maximum Depth of Binary Tree 二叉树的最大深度(简单)
    leetcode 105. Construct Binary Tree from Preorder and Inorder Traversal 从前序与中序遍历序列构造二叉树(中等)
    leetcode 83. Remove Duplicates from Sorted List 删除排序链表中的重复元素(简单)
    leetcode 637. Average of Levels in Binary Tree 二叉树的层平均值(简单)
    Fiddler的安装与使用
    Redis
    开发那些事儿:如何解决js打包文件体积过大导致的网页加载慢问题?
    AI人工智能识别技术如何助力构建风险监测预警系统?
    H.265流媒体播放器EasyPlayer切换播放协议时,快照无法消失如何处理?
    AI人脸检测/行为识别智能分析网关8大智慧应用场景分析
  • 原文地址:https://www.cnblogs.com/zsw-1993/p/4879080.html
Copyright © 2020-2023  润新知