美文网首页iOS开发进阶ios学习资料iOS
UIWebView与JavaScript的交互及JavaScri

UIWebView与JavaScript的交互及JavaScri

作者: WillPan | 来源:发表于2015-11-27 17:53 被阅读3166次

    UIWebView与JS的交互,说白了就是Objective-C和JavaScript的相互调用。Objective-C调用JavaScript代码的方法,是通过UIWebView的 - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;的方法来实现的。该方法向UIWebView传递一段需要执行的JavaScript代码最后获取执行结果。

    JavaScript调用Objective-C的方法,并没有现成的API,但是有些方法可以达到相应的效果。具体是利用UIWebView的特性:在UIWebView的内发起的所有网络请求,都可以通过delegate函数得到通知。

    说明:

    • 本文是一个小白记录OC与JS交互的学习历程,适合跟我一样的小白,大神若要喷,请轻喷_

    • 学习UIWebView与JS的交互之前,建议熟悉下HTML和Javascript的基本语法,不用看太多,在w3school看一到两天HTML,再看一到两天JS就行。

    OC调用JS方法、JS调用OC方法(不使用第三方开源库的情况下)

    准备工作:

    1.新建一个Single View Application,
    再新建一个ViewController(eg:BasicUsageViewController),然后在StoryBoard新建一个ViewController,拖一个UIWebView和UILabel以备用,关联webView及代理

    @property (weak, nonatomic) IBOutlet UIWebView *webView;
    @property (weak, nonatomic) IBOutlet UILabel *testLabel;
    

    2.在工程中新建一个web1.html文件(Commend+N、OtherEmptyNext、输入、create),代码如下:

    <!DOCTYPE html>
    <html>
        <head>
            <meta charset="UTF-8">
                <title>我是HTML标题</title>
                </head>
        <body bgcolor="#9aff9a">
            <div id="addNewNodeTest">
                <p id="p1"> 这是段落A。</p>
                <p id="p2"> 这是段落B。</p>
            </div>
            <div class="page">
                <button onclick="changeUILabelText()"> 改变UILabel文字 </button>
                <button onclick="logText()"> NSLog打印文字 </button>
            </div>
        </body>
        
    </html>
    

    3.再在工程中新建一个test.js文件,代码如下:

    //添加子节点
    function addNewNodeTest () {
        var para = document.createElement("p");
        var node = document.createTextNode("这是新段落。");
        para.appendChild(node);
        var element = document.getElementById("addNewNodeTest");
        element.appendChild(para);
        console.log("添加子节点成功");
    }
    
    //改变UILabel的文本
    function changeUILabelText() {
        //"changelabeltext"是你自己定的一个协议。
        //注url不要含大写字母,就算是大写字母,在`webView:shouldStartLoadWithRequest:navigationType:`代理方法里也会被替换成小写字母
        var url = "changelabeltext:" + "我是改变后的文字";
        //给document.location重新赋值就相当于webView加载一个新的URL,所以又会调用`webView:shouldStartLoadWithRequest:navigationType:`方法,然后就可以在这个代理方法里截获这个重定向请求
        document.location = url;
    }
    
    //也可以自己封装个传参数的方法
    function sendCommand(cmd,param){
        var url = "yourprotocol:" + cmd + ":" + param;
        document.location = url;
    }
    //打印测试
    function logText(){
        sendCommand("log","Hi,I'm In logText Function");
    }
    

    好了,现在可以开撸了

    4.加载webView并插入测试js

    - (void)viewDidLoad {
        [super viewDidLoad];
        // Do any additional setup after loading the view.
        NSString* path = [[NSBundle mainBundle] pathForResource:@"web1" ofType:@"html"];
        [self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:path]]];
        
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"test" ofType:@"js"];
        NSString *jsString = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
        [self.webView stringByEvaluatingJavaScriptFromString:jsString];
    }
    

    5、加载结束,获取HTML页面title元素,赋值给self.title

    - (void)webViewDidFinishLoad:(UIWebView *)webView {
        // 获取HTML页面title元素,赋值给self.title
        self.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
    }
    

    6、建几个按钮,体验插入js的几种方式

    模拟器
    //分别对应上图3个按钮
    - (IBAction)insertJavaScript1:(UIButton *)sender {
        //方法1:预加载的test.js内部已经写了addNewNodeTest()方法,这里只需注入"addNewNodeTest()"字符串即可
        [self.webView stringByEvaluatingJavaScriptFromString:@"addNewNodeTest()"];
    }
    
    - (IBAction)insertJavaScript2:(UIButton *)sender {
        //方法2:把test.js内部的addNewNodeTest()方法复制过来,去掉行与行之间的空格
        //字符串双引号要么前面加转义符"\",要么变成单引号,例如:
        NSString *addNewNode = @"var para = document.createElement(\"p\");var node=document.createTextNode('这是新段落。');para.appendChild(node);var element=document.getElementById('addNewNodeTest');element.appendChild(para);";
        [self.webView stringByEvaluatingJavaScriptFromString:addNewNode];
    }
    
    - (IBAction)insertJavaScript3:(UIButton *)sender {
        //方法3:把test.js内部的addNewNodeTest()方法复制过来,并在每一行首尾加上双引号(跟方法2差不多)
        NSString *addNewNode =
        @"var para = document.createElement('p');"
        "var node = document.createTextNode('这是新段落。');"
        "para.appendChild(node);"
        "var element = document.getElementById('addNewNodeTest');"
        "element.appendChild(para);";
        [self.webView stringByEvaluatingJavaScriptFromString:addNewNode];
    }
    

    说明:addNewNodeTest()方法执行的操作是创建了一个节点<p> 这是新段落。</p>,添加到了位置1,然后webView上就会新增一行,不懂的同学请自行脑补(看不懂也没关系,这里只是演示怎么用OC调js代码)

        <div id="addNewNodeTest">
            <p id="p1"> 这是段落A。</p>
            <p id="p2"> 这是段落B。</p>
            //位置1
        </div>
    
    

    7.好了,现在让js调OC的方法:在ViewController里添加如下代码:

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
        NSLog(@"开始加载请求");
        //当点击按钮时,navigationType = UIWebViewNavigationTypeOther
        NSString *requestString = [[request URL] absoluteString];
        NSArray *components = [requestString componentsSeparatedByString:@":"];
        if ([components[0] isEqualToString:@"changelabeltext"] && components.count > 1) {
            //这种通过URL传参数的方式貌似不是太好,因为参数如果含中文还得URL解码,eg:
            self.testLabel.text = [components[1] stringByRemovingPercentEncoding];
            return NO;
        }
        //也可以这样判断
        else if([request.URL.scheme isEqualToString:@"yourprotocol"]) {
            NSLog(@"%@",[components[2] stringByRemovingPercentEncoding]);
            return NO;
        }
        return YES;
    }
    

    点击webView里的改变UILabel文字按钮会发现testLabel的文字变了,这里解释下原因:web1.html代码中
    <button onclick="changeUILabelText()"> 改变UILabel文字 </button>这个按钮绑定了一个方法,名字叫changeUILabelText(),点击就会调用changeUILabelText()方法(当然包含这个方法的test.js已经加载了),然后webView的URL变了就会重新加载,这样在回调方法webView:shouldStartLoadWithRequest:navigationType:会再次调用,然后就可以在这个代理方法里截获这个重定向请求的request.URL.absoluteString来处理OC代码了

    说明:

    (1)Objective-C调用JavaScript代码的时候是同步的

    - (NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

    (2)JavaScript调用Objective-C代码的时候是异步的

    - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

    调试模拟器或真机里的WebView的技巧

    • 模拟器

    模拟器加载网页后,打开电脑端Safari(确保偏好设置里的高级 - 在菜单中显示“开发”菜单选项已打开),然后选择开发Simulator,就会看见模拟器正在运行的web1.html,点击web1.html就来到了控制台。
    点击方式一按钮,就会发现控制台标签页最先面有输出,这是因为在test.jsaddNewNodeTest ()的最后一行有这么一句话:console.log("添加子节点成功");,在这里,console.log()相当于NSLog,括号内可以直接加变量。

    当然,你也可以在控制台插入js代码,如下图:在左下角输入一句js代码alert('666');就能在模拟器上得到反馈,当然,此时你输入addNewNodeTest();效果跟点击方式一按钮是一样的

    模拟器调试之控制台

    你也可以切换到调试器标签,然后打个断点,点击方式一按钮,就可以单步调试了。有兴趣的同学可以切换到元素标签页看看

    模拟器调试之调试器
    • 真机

    首先在手机的设置 - Safari - 高级 - 启用Web检查器,然后用数据线连接电脑,Xcode运行你的项目,打开一个含webView的页面,就可以在电脑端Safari的开发菜单下看到你的设备了,调试方法同上

    高级用法(WebViewJavascriptBridge)

    WebViewJavascriptBridge 是一个用于UIWebView / WKWebViews和JS交互的封装库,连Facebook Messenger都在使用。

    这里我就引用一下杨骑滔的这篇博客的内容,也就是通过实现以下功能来学习WebViewJavascriptBridge的使用(侵删)。

    原文已经比较详尽了,但是有一些地方对于我等小白来说可能不够详细,所以折腾了不少时间,所以在这里对原文做了一点修改,更加清晰易懂。

    要实现的功能

    • WebView展示一段HTML,禁止HTML文本中自带的 <img> 标签自动加载,也就是说下载图片的操作放在native端来处理,并通过JS将图片在Cache中的地址返回给UIWebview。

    • 实现点击WebView图片放大、保存图片到相册等操作。

    之所以要把图片操作放在native端做的好处在于:1、可以进行本地缓存,下次进入这篇文章可以直接从缓存读取,提高响应速度并且节省用户流量。2、可以实现点击图片放大、保存图片到相册等操作。

    技术难点也有两个:

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

    先来看看基本用法

    在WebViewJavascriptBridge中,交互的方式只有两种:send 和 callHandler,JS和OC都有这两个方法,所以对应的四种关系是(很重要):

    四种关系图表

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

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

    也就是说,一种语言register了Handler(回调或者block),另一种语言callHandler就会执行回调或者block,还能传递数据;不理解不要紧,下面的Demo这四种方式全都有例子。

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


    整体思路

    废话不说,直接开撸:

    1、导入WebViewJavascriptBridge,新建一个ViewController,声明一个WebViewJavascriptBridge实例:

    @property WebViewJavascriptBridge* bridge;
    

    2、找一个含图片的html,比如这一篇(源码已做删减),导入到项目中

    3、在项目中新建一个js文件,比如imageCache.js,贴上如下代码:

    //一加载这个js就会调用下面自己写的onLoaded() 方法
    window.onload = function() {
        onLoaded();
    }
    
    //使用WebViewJavascriptBridge的话,这一段是必须的(固定写法)
    function connectWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) {
            callback(WebViewJavascriptBridge)
        } else {
            document.addEventListener('WebViewJavascriptBridgeReady', function() {
                                      callback(WebViewJavascriptBridge)
                                      }, false)
        }
    }
    
    //上面已经说了,一插入js,这个方法就开始执行
    function onLoaded() {
        connectWebViewJavascriptBridge(function(bridge) {
                                       //document.querySelectorAll:按文档顺序返回指定元素节点的子树中匹配选择器的元素集合,如果没有匹配返回空集合
                                       //下面这几句是提取所有img标签的esrc属性值(图片的URL),并存到imageUrlsArray这个数组中
                                       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);
                                                        });
                                       //将imageUrlsArray这个数组发送到OC的block
                                       bridge.send(imageUrlsArray);////四种关系图表之第1种
                                       
                                       bridge.init(function(message, responseCallback) {
                                                   alert(message);
                                                   if (responseCallback) {
                                                   responseCallback("Message1已收到,送你个Message2")
                                                   }
                                        })
                                       //这里先注册下,等待OC代码的_bridge调用([_bridge callHandler:....])
                                       bridge.registerHandler('imagesDownloadComplete', function(data, responseCallback) {
                                                              var allImage = document.querySelectorAll("img");
                                                              allImage = Array.prototype.slice.call(allImage, 0);
                                                              allImage.forEach(function(image) {
                                                                               if (image.getAttribute("esrc") == data[0] || image.getAttribute("esrc") == decodeURIComponent(data[0])) {
                                                                               image.src = data[1];
                                                                               }
                                                                               
                                                                               });
                                                              responseCallback("图片"+data[0]+"已加载")
                                                              })
                                       
                                       //使用WebViewJavascriptBridge的话,这一段是必须的,不然上面的imageUrlsArray传不过去
                                       bridge.send('Please respond to this', function responseCallback(responseData) {
                                                   console.log("Javascript got its response", responseData)
                                                   })
                                       });
        
    }
    

    4、viewDidLoad里加载 webView

    NSString *path = [[NSBundle mainBundle] pathForResource:@"article1" ofType:@"html"];
        //原网页html代码
        NSString *_content = [NSString stringWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
        //我们要做的第一步是替换获取的HTML文本中默认的src,避免webView自动加载图片
        _content = [_content stringByReplacingOccurrencesOfString:@"src" withString:@"esrc"];
        //正则替换,给每个图片添加一个onImageClick点击方法
        NSRegularExpression *regex = [NSRegularExpression regularExpressionWithPattern:@"(<img[^>]+esrc=\")(\\S+)\"" options:0 error:nil];
        //终于得到我想要的html了!!!
        _content = [regex stringByReplacingMatchesInString:_content options:0 range:NSMakeRange(0, _content.length) withTemplate:@"<img esrc=\"$2\" onClick=\"javascript:onImageClick('$2')\""];
        [self.webView loadHTMLString:_content baseURL:nil];
        
        //插入js
        NSString *filePath = [[NSBundle mainBundle] pathForResource:@"imageCache" ofType:@"js"];
        NSString *jsString = [[NSString alloc] initWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil];
        [self.webView stringByEvaluatingJavaScriptFromString:jsString];
        
        //初始化一个WebViewJavascript桥梁,方便imageCache.js把数据传过来
        self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.webView webViewDelegate:self handler:^(id data, WVJBResponseCallback responseCallback) {
            NSLog(@"###来自imageCache.js的图片URL数组: %@", data);
            //利用SDWebImageManager下载图片到本地
            [self downloadAllImagesInNative:data];
            _imageURLs = data;
            responseCallback(@"###Right back atcha");
        }];
    
    #pragma mark -- 下载全部图片
    -(void)downloadAllImagesInNative:(NSArray *)imageUrls{
        SDWebImageManager *manager = [SDWebImageManager sharedManager];
        //初始化一个数组用于存image
        _allImagesOfThisArticle = [NSMutableArray arrayWithCapacity:imageUrls.count];
        for (NSUInteger i = 0; i < imageUrls.count; 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) {
                    [_allImagesOfThisArticle addObject: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];
                        //四种关系图表之第4种
                        [_bridge callHandler:@"imagesDownloadComplete" data:@[key,source] responseCallback:^(id responseData) {
                            NSLog(@"js把img标签的esrc属性替换后-->%@<",responseData);
                        }];
                        
                    });
                    
                }
                
            }];
            
        }
        
    }
    

    好了,到这里为止就webView就可以加载本地缓存的图片了,如果要实现图片的点击放大,请接着往下看

    5、imageCache.js里添加图片点击事件:

    //图片点击会触发
    function onImageClick(picUrl){
        connectWebViewJavascriptBridge(function(bridge) {
                                       //作者用的是"p img[esrc]",意思是获取p标签里的img的src值
                                       //我这里的图片是div,所以要改成"div img[esrc]"
                                       //var allImage = document.getElementsByTagName('img');//这样比较通用
                                       var allImage = document.querySelectorAll("div 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;
                                       //获取点击图片在所有图片中的编号以及在图片相对于webView左上角的位置、宽高,并把这些信息返回给OC
                                       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+";width:"+image.width +";height:"+image.height);
                                                        }
                                                        });
                                       
                                       console.log("检测到点击"+"x="+x+"y="+y+"width="+width+"height="+height);
                                       //四种关系图表之第2种
                                       bridge.callHandler('imageDidClicked', {'index':index,'x':x,'y':y,'width':width,'height':height}, function(response) {
                                                          console.log("JS已经发出imgurl和index,同时收到回调,说明OC已经收到数据");
                                                          });
                                       });
        
    }
    
    

    6、viewDidLoad里注册js图片点击事件回调,这里我用了一个简单的图片浏览器HZPhotoBrowser,修改了部分代码使能够适用于webView

    //这里注册一下,imageCache.js里的`bridge.callHandler('imageDidClicked', {'index':index,'x':x,'y':y,'width':width,'height':height}, function(response)`就会传数据过来
        [_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];
            
            //启动图片浏览器
            HZPhotoBrowser *browserVc = [[HZPhotoBrowser alloc] init];
            // browserVc.sourceImagesContainerView = cell.webView; // 原图的父控件
            browserVc.imageCount = _allImagesOfThisArticle.count; // 图片总数
            browserVc.currentImageIndex = index;
            browserVc.delegate = self;
            browserVc.imageFrameinWebView = CGRectMake(originX, originY+64, width, height);
            [browserVc show];
            
            NSLog(@"OC已经收到JS的imageDidClicked了: %@", data);
            responseCallback(@"OC已经收到JS的imageDidClicked了");
        }];
        //四种关系图表之第3种(测试)
    //    [_bridge send:@"###Message1:我将会被发送到imageCache.js里bridge.init()的回调里"];
        
        //四种关系图表之第3种(测试)
    //    [_bridge send:@"###Message1:我将会被发送到imageCache.js里bridge.init()的回调里,imageCache.js还会给我回调,不信你可能下面的Log" responseCallback:^(id responseData) {
    //        NSLog(@"###%@", responseData);
    //    }];
    
    #pragma mark - HZPhotoBrowser的代理方法
    //这里没有占位小图,所以就让大图代替
    - (UIImage *)photoBrowser:(HZPhotoBrowser *)browser placeholderImageForIndex:(NSInteger)index {
        return _allImagesOfThisArticle[index];
    }
    
    - (NSURL *)photoBrowser:(HZPhotoBrowser *)browser highQualityImageURLForIndex:(NSInteger)index {
        return [NSURL URLWithString:_imageURLs[index]];
    }
    

    最终效果

    最终效果

    好了,到此为止WebViewJavascriptBridge的基本用法已基本说完了,虽然很简单,但是也花了我一天的时间,写的同时又发现了不少新东西,还是很值的。这里是这个小Demo的源码。渣渣代码,就不上传github了。

    • 参考及推荐文章:

    UIWebView与JS的深度交互

    UIWebView与JavaScript的那些事儿

    iOS开发之Objective-C与JavaScript的交互

    关于UIWebView和PhoneGap的总结

    相关文章

      网友评论

      本文标题:UIWebView与JavaScript的交互及JavaScri

      本文链接:https://www.haomeiwen.com/subject/abvdhttx.html