UIWebView & WKWebView 详解上

前言

一直想系统的总结下UIWebViewWKWebView,这里整理了一个
Demo可供参考

分为两部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下

OC-->JS

UIWebView OC-->JS

  • 1、通过调用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;方法
  • 2、在页面加载完成后,获取JSContext上下文,通过JSContext的- (JSValue *)evaluateScript:(NSString *)script;方法得到JSValue对象,JSValue对象可转为Array、Number、String、对象等数据类型

WKWebView OC-->JS

通过调用方法:- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler;

UIWebView OC-->JS解析

  • - (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;使用:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    //@property (nonatomic, strong) UIWebView * webView;
    self.title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"];
}

1、该方法不能判断调用了一个js方法之后,是否发生了错误。当错误发生时,返回值为nil,而当调用一个方法本身没有返回值时,返回值也为nil,所以无法判断是否调用成功了。
2、返回值类型为nullable NSString *,就意味着当调用的js方法有返回值时,都以字符串返回,不够灵活。当返回值是一个js的Array时,还需要解析字符串,比较麻烦。

  • - (JSValue *)evaluateScript:(NSString *)script;使用:
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    //@property (nonatomic, strong) UIWebView * webView;
    //@property (nonatomic, strong) JSContext * jsContext;
    //获取该UIWebview的javascript上下文
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    //JSContext oc调用js
    JSValue *value = [self.jsContext evaluateScript:@"document.title"];
    self.title = value.toString;
}

1、 其实WebKit都有一个内嵌的js环境,一般我们在页面加载完成之后,获取js上下文,然后通过JSContext的evaluateScript:方法来获取返回值。因为该方法得到的是一个JSValue对象,所以支持JavaScript的Array、Number、String、对象等数据类型。该方法解决了stringByEvaluatingJavaScriptFromString:返回值只是NSString的问题。

2、 [self.jsContext evaluateScript:@"document.titlexxxx"];那么必然会报错,报错了,可以通过 @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);,设置该block来获取异常

//在调用前,设置异常回调
 [self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
     NSLog(@"%@", exception);
 }];
     //执行方法
 JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];

该方法,也很好的解决了stringByEvaluatingJavaScriptFromString:调用js方法后,出现错误却捕获不到的缺点。

WKWebView OC-->JS解析

//@property (strong, nonatomic) WKWebView *webView;
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
//    self.title = self.webView.title;
//     //执行一段js,并将结果返回,如果出错,error则不为空
    [self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        self.title = title;
    }];
}

该方法很好的解决了UIWebView使用stringByEvaluatingJavaScriptFromString:方法的两个缺点(1. 返回值只能是NSString。2. 报错无法捕获)。


JS-->0C

UIWebView JS-->0C

1、拦截URL
OC中,只要遵循了UIWebViewDelegate协议, 每次打开一个链接之前,都会触发方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在该方法中,捕获该链接,并且返回NO(阻止本次跳转),从而执行对应的OC方法。

2、self.jsContext[@"yourMethodName"] = your block;其中yourMethodName就是js的方法名称,赋给是一个block 里面是oc代码

WKWebView JS-->OC

1、URL拦截
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

2、WKUserContentController中新增方法

  • 注册回调 - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
  • js中调用方法 window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
  • oc中将会收到WKScriptMessageHandler的回调
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
  • 移除
    - (void)removeScriptMessageHandlerForName:(NSString *)name;

拦截URL解析

1、Html中实现
比如syhcandy://方法是在html或者js中,点击某个按钮触发事件时,跳转到自定义URL Scheme构成的链接,而Objective-C中捕获该链接,从中解析必要的参数,实现JS到OC的一次交互。比如页面中一个a标签,链接如下:

<a href="syhcandy://smsLogin?username=syh&code=776632">短信验证登录</a>

UIWebView的JS-->OC中URL拦截实现

而在Objective-C中,只要遵循了UIWebViewDelegate协议,那么每次打开一个链接之前,都会触发方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在该方法中,捕获该链接,并且返回NO(阻止本次跳转),从而执行对应的OC方法。

#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    //标准的URL包含scheme、host、port、path、query、fragment等
    NSURL *URL = request.URL;
    if ([URL.scheme isEqualToString:SHWebViewDemoScheme]) {
        if ([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]) {
            NSLog(@"短信验证码登录,参数为 %@", URL.query); //短信验证码登录,参数为 username=syh&code=776632
            return NO;
        }
    }
    return YES;
}

缺点:无法直接获取本次交互的返回值,比较适合单向传参,且不关心回调的情景,比如h5页面跳转到native页面等。

WKWebView JS-->OC中URL拦截实现

当用户点击这个a标签时,会被拦截

//针对一次action来决定是否允许跳转,允许与否都需要调用decisionHandler,比如decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    //可以通过navigationAction.navigationType获取跳转类型,如新链接、后退等
    NSURL *URL = navigationAction.request.URL;
    //判断URL是否符合自定义的URL Scheme
    if([URL.scheme isEqualToString:SHWebViewDemoScheme]){
        //根据不同的业务,来执行对应的操作,且获取参数
        if([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]){
            NSString *param = URL.query;
            NSLog(@"短信验证码登录, 参数为%@", param);//短信验证码登录, 参数为username=12323123&code=892845
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

html中定义了分享方法如下:

<div>
<a href="javascript:void(0);" class="sharebtn" onclick="share('分享标题', 'http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg', location.href)">分享活动,领30元红包</a>
</div>
<script>
    //简单分享
    function share (title, imgUrl, link) {
        //便于WKWebView测试
        window.webkit.messageHandlers.share.postMessage({title: title, imgUrl: imgUrl, link: link});
        //这里需要OC实现
    }
</script>

UIWebView JS-->OC中OC实现

self.jsContext[@"yourMethodName"] = your block;方法映射
在页面加载完成时,先获取js上下文。获取到之后,我们就可以进行强大的方法映射了。

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
    //其中share就是js的方法名称,赋给是一个block 里面是iOS代码
    //此方法最终将打印出所有接收到的参数,js参数是不固定的
    self.jsContext[@"share"] = ^(){
        NSArray *args = [JSContext currentArguments];//获取到share里的所有参数
        //args中的元素是JSValue,需要转成OC的对象
        NSMutableArray *messages = [NSMutableArray array];
        for (JSValue *obj in args) {
            [messages addObject:[obj toObject]];
        }
        NSLog(@"点击分享js传回的参数:\n%@", messages);
        /**
         点击分享js传回的参数:
         (
         "\U5206\U4eab\U6807\U9898",
         "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg",
         "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/2684F36D-58CB-4A37-96AB-334D21098682/WebViewDemo.app/test.html"
         )
         */
    };
}

scriptMessageHandler

WKUserContentController.h新增两个方法如下:

//在OC中添加一个scriptMessageHandler,则会在all frames中添加一个js的function: window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 。
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
- (void)removeScriptMessageHandlerForName:(NSString *)name;

WKWebView JS-->OC中OC实现

  • 在OC中添加一个handler
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注册回调
 [UserContentController addScriptMessageHandler:self name:@"share"];
  • js调用share方法后,OC会收到WKScriptMessageHandler回调
#pragma mark - WKScriptMessageHandler  js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"share"]) {
        id body = message.body;
        NSLog(@"share分享的内容为:%@", body);
        /**
         share分享的内容为:{
         imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
         link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/C009579D-10B9-49D2-A3A4-4D409157C158/WebViewDemo.app/test.html";
         title = "\U5206\U4eab\U6807\U9898";
         }
         */
    }
}
  • 记得移除注册的回调
- (void)dealloc
{
    //记得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
}

需求:写一个有两个参数,一个返回值的js方法,oc应该怎么替换呢?

Html中

<div onclick="alert(testAddMethod(1,5))">点击测试两数相加</div>
<script>
    //该方法传入两个整数,求和,并返回结果
    function testAddMethod (a, b) {
        //需要OC实现a+b,并返回
        return a + b;
    }
</script>

UIWebView中对应的OC替换该方法的实现:

self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b){
     return a + b;
//   return a * b;
};

需求升级:调用方法原实现,并且在原结果上乘以10

//调用方法的本来实现,给原结果乘以10
JSValue *value = self.jsContext[@"testAddMethod"];
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b){
    JSValue *resultValue = [value callWithArguments:[JSContext currentArguments]];
    return resultValue.toInt32 * 10;
};

新需求:h5中有一个分享按钮,用户点击之后,调用native分享(微信分享、微博分享等),在native分享成功或者失败时,回调h5页面,告诉其分享结果,h5页面刷新对应的UI,显示分享成功或者失败。

Html的代码

<a href="javascript:void(0);" onclick="test()">测试新分享</a></br>
<h>下面展示分享结果</p><div id="shareResult"></div>
<script>
/**
 * 分享方法,并且会异步回调分享结果
 * @param  {对象类型} shareData 一个分享数据的对象,包含title,imgUrl,link以及一个回调function
 * @return {void}     无同步返回值
 js的shareNew方法的参数是一个对象,该对象包含了几个必要的字段,以及一个回调函数,这个回调函数有点像oc的block,调用者把一个function传入一个function当作参数,在适当时候,方法内实现者调用该function,实现对调用者的异步回调。
 */
function shareNew(shareData) {
    var title = shareData.title;
    var imgUrl = shareData.imgUrl;
    var link = shareData.link;
    var result = shareData.result;
    //do something
    //这里模拟异步操作
    setTimeout(function(){
        //2s之后,回调true分享成功
        result(true);
    }, 2000);     
    //用于WKWebView,因为WKWebView并没有办法把js function传递过去,因此需要特殊处理一下
    //把js function转换为字符串,oc端调用时 (<js function string>)(true); 即可
    shareData.result = result.toString();
    window.webkit.messageHandlers.shareNew.postMessage(shareData);
}

function test() {
    //清空分享结果
    shareResult.innerHTML = "";
    
    //调用时,应该
    shareNew({
             title: "title",
             imgUrl: "http://img.dd.com/xxx.png",
             link: location.href,
             result: function(res) {
                //这里shareResult 等同于 document.getElementById("shareResult")
                shareResult.innerHTML = res ? "success" : "failure";

             }
    });
}
</script>
上述html代码解析:

点击之后,触发了test()函数,test()中封装了对shareNew()函数的调用,且传了一个对象作为参数,对象中result字段对应的是个匿名函数,紧接着shareNew()函数调用,其中的实现是2s过后,result(true);模拟js异步实现异步回调结果,分享成功。同时shareNew()函数中,因为通过scriptMessageHandler无法传递function,所以先把shareData对象中的result这个匿名function转成String,然后替换shareData对象的result属性为这个String,并回传给OC,OC这边对应JS对象的数据类型是NSDictionary,我们打印并得到了所有参数,同时,把result字段对应的js function String取出来。这里我们延迟4s回调,模拟Native分享的异步过程,在4s后,也就是js中显示success的2s过后,调用js的匿名function,并传递参数(分享结果)。调用一个js function的方法是 functionName(argument);,这里由于这个js的function已经是一个String了,所以我们调用时,需要加上(),如 (functionString)(argument);因此,最终我们通过OC -> JS 的evaluateJavaScript:completionHandler:方法,成功完成了异步回调,并传递给js一个分享失败的结果。

上面的描述看起来很复杂,其实就是先执行了JS的默认实现,后执行了OC的实现。上面的代码展示了如何解决scriptMessageHandler的两个问题,并且实现了一个 JS -> OC、OC -> JS 完整的交互流程。

UIWebView在OC中的实现:

- (void)convertJSFunctionsToOCMethods
{
        self.jsContext[@"shareNew"] = ^(JSValue *shareData){//首先这里要注意,回调的参数不能直接写NSDictionary类型,为何呢?
        //仔细看,打印出的确实是一个NSDictionary,但是result字段对应的不是block而是一个NSDictionary
        NSLog(@"%@", [shareData toObject]);
        /**
         {
         imgUrl = "http://img.dd.com/xxx.png";
         link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/F0F701AB-D134-4596-8104-16EDD27CDBCD/WebViewDemo.app/test.html";
         result =     {
         };
         title = title;
         }
         */
        //获取shareData对象的result属性,这个JSValue对应的其实是一个javascript的function。
        JSValue *resultFunction = [shareData valueForProperty:@"result"];
        //回调block,将js的function转换为OC的block
        void (^result)(BOOL) = ^(BOOL isSuccess) {
            [resultFunction callWithArguments:@[@(isSuccess)]];
        };
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            result(NO);
            
        });
    };
}

WKWebView在OC中的实现

  • 注册回调
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注册回调
[UserContentController addScriptMessageHandler:self name:@"shareNew"];
  • WKScriptMessageHandler代理调用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
     if ([message.name isEqualToString:@"shareNew"]){
        NSDictionary *shareData = message.body;
        NSLog(@"%@分享的数据为: %@", message.name, shareData);
        /**
         shareNew分享的数据为: {
         imgUrl = "http://img.dd.com/xxx.png";
         link = "file:///Users/macair/Library/Developer/CoreSimulator/Devices/04C1A1B2-EBF1-4C3A-BC06-6664428718F6/data/Containers/Bundle/Application/9DE46251-970B-460B-AB13-86629C09C279/WebViewDemo.app/test.html";
         result = "function (res) {\n                            //\U8fd9\U91ccshareResult \U7b49\U540c\U4e8e document.getElementById(\"shareResult\")\n                            shareResult.innerHTML = res ? \"success\" : \"failure\";\n\n                         }";
         title = title;
         }
         */
        
        //模拟异步回调
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //读取js function的字符串
            NSString *jsFunctionString = shareData[@"result"];
            /*
             function (res) {
             //这里shareResult 等同于 document.getElementById("shareResult")
             shareResult.innerHTML = res ? "success" : "failure";
             
             }
             */
            //拼接调用该方法的js字符串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO];    //后面的参数NO为模拟分享失败
            //执行回调
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                if (!error) {
                    NSLog(@"模拟回调,分享失败");
                }
            }];
        });
    }
}
  • 移除注册
- (void)dealloc
{
    //记得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"shareNew"];
}
Native预览H5页面中的image

场景:在微信中浏览网页时,看到喜欢的图片,你会点击图片查看大图,然后长按图片保存。

分析

1、如果想在Native预览H5中的image,最需要的是什么?是图片的链接。如果能有缩略图更好了。
2、只要获取了链接,就可以跳转到一个ViewController中,预览图片,后续长按保存自然水到渠成。
3、那应该如何获取图片的链接呢?通过JS -> OC 传递图片url

方案

当页面加载完成后,给html页面中所有无默认点击事件的<img>添加点击事件,当用户点击时,拿到所有参数。
(其实这不是最好的方案,最好的解决方案是,跟前端约定一下,哪些图片需要预览,哪些img标签的id统一,或者有个特定的属性,这样客户端可以根据id找到这些img标签)

Html中有img标签

 <img src="http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg">

写好一个ImgAddClickEvent.js文件,来实现给所有无默认点击事件的<img>添加点击事件。

//获取所有img标签
var imgs = document.getElementsByTagName("img");
//获取所有的imgUrl
var imgUrls = new Array();
var x = 0;
var y = 0;
var width = 0;
var height = 0;
for (var i = 0; i < imgs.length; i++) {
    var img = imgs[i];
    //如果图片链接存在
    if (img.src || img.getAttribute('data-src')) {
        //添加到图片链接数组中
        imgUrls.push(img.src || img.getAttribute('data-src'));
        //如果图片没有默认的onclick事件,且父元素不是a标签,则添加onclick事件,当用户点击时,把图片链接回传给Native
        if (!img.onclick && img.parentElement.tagName !== "A") {
            //给图片添加下标的属性
            img.index = i;      //记录下标
            //添加点击事件,并且回传选中的图片链接、下标、屏幕上的位置、全部的图片数组等
            img.onclick = function() {
                x = this.getBoundingClientRect().left;
                y = this.getBoundingClientRect().top;
                x = x + document.documentElement.scrollLeft;
                y = y + document.documentElement.scrollTop;
                width = this.width;
                height = this.height;
                var imgInfo = {
                imgUrl: this.src || this.getAttribute('data-src'),
                x: x,
                y: y,
                width: width,
                height: height,
                index: this.index,
                imgUrls: imgUrls
                };
                //UIWebView使用
                h5ImageDidClick(imgInfo);
            }
        }
    }
}
function h5ImageDidClick (info){
    //WKWebView使用
    window.webkit.messageHandlers.imageDidClick.postMessage(info);
}
UIWebView实现

UIWebView直接使用JavaScriptCore<img>添加onclick方法为OC的实现即可。

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
    //获取该UIWebview的javascript上下文
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    /*
     Native预览H5页面中的image
     */
    //防止频繁IO操作,造成性能影响
    static NSString *jsSource;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImgAddClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
    });
    //先注入给图片添加点击事件的js
    [self.jsContext evaluateScript:jsSource];
    self.jsContext[@"h5ImageDidClick"] = ^(NSDictionary *imgInfo){
        NSLog(@"UIWebView点击了html上的图片,信息是:%@", imgInfo);
    };
    /**
     UIWebView点击了html上的图片,信息是:{
     height = 168;
     imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
     imgUrls =     (
     "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
     );
     index = 0;
     width = 252;
     x = 8;
     y = 8;
     }
     */
}

WKWebView实现

添加脚本

/**
 Native预览H5页面中的image,
 页面中的所有img标签添加点击事件
 */
- (void)imgAddClickEvent
{
    //防止频繁IO操作,造成性能影响
    static NSString *jsSource;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImgAddClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
    });
    /*
     注入的js source可以是任何js字符串,也可以js文件。比如你有很多提供给h5使用的js方法,那么你本地可能就会有一个ImgAddClickEvent.js
     */
    //添加自定义的脚本
    WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    [self.webView.configuration.userContentController addUserScript:js];
    //注册回调
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];

}

注册回调后的代理方法

#pragma mark - WKScriptMessageHandler  js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"imageDidClick"]) {
        //点击了html上的图片, Native预览H5页面中的image
        NSLog(@"点击了html上的图片,参数为%@", message.body);
        /**
         点击了html上的图片,参数为{
         height = 168;
         imgUrl = "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg";
         imgUrls =     (
         "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
         );
         index = 0;
         width = 252;
         x = 8;
         y = 8;
         }

         注意这里的x,y是不包含自定义scrollView的contentInset的,如果要获取图片在屏幕上的位置:
         x = x + self.webView.scrollView.contentInset.left;
         y = y + self.webView.scrollView.contentInset.top;
         */
        
        NSDictionary *dict = message.body;
        NSString *selectedImageUrl = dict[@"imgUrl"];
        CGFloat x = [dict[@"x"] floatValue] + + self.webView.scrollView.contentInset.left;
        CGFloat y = [dict[@"y"] floatValue] + self.webView.scrollView.contentInset.top;
        CGFloat width = [dict[@"width"] floatValue];
        CGFloat height = [dict[@"height"] floatValue];
        CGRect frame = CGRectMake(x, y, width, height);
        NSUInteger index = [dict[@"index"] integerValue];
        NSLog(@"点击了第%@个图片,\n链接为%@,\n在Screen中的绝对frame为%@,\n所有的图片数组为%@", @(index), selectedImageUrl, NSStringFromCGRect(frame), dict[@"imgUrls"]);
        /*
         点击了第0个图片,
         链接为http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg,
         在Screen中的绝对frame为{{8, 72}, {252, 168}},
         所有的图片数组为(
         "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
         )
         */

    }
    
}

移除

- (void)dealloc
{
    //记得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"imageDidClick"];
}

Native为H5提供一套Native Api(微信、支付宝小程序)

很多时候,Native与H5交互得深了,必定会有一些更深层次的需求。比如h5想控制页面的pop、push、present,想调用Native的Share,想调用Native的扫描二维码功能,获取扫描结果……

微信提供的一些Api(扫码、选择照片等)都是如何实现的呢?很明显,native提供的。

首先,我们为H5提供一套Api,那自然Api是暴露给js的,所以这些Api也是js的。下面针对一些需求,分析下封装和实现。其中用到了js闭包,需要一点js知识。

从通讯录选择联系人

从通讯录选择联系人的例子,从js到native,再从native到js。

js端实现

/**
 * Native为H5提供的Api接口
 *
 * @type {js对象}
 */
var DANativeApi = (function() {

    var NativeApi = {
        /**
         * 从通讯录选择联系人
         * @return {void} 无同步返回值,异步返回选择的结果
         */
        choosePhoneContact: function(param) {
            //具体是否需要判断
            //调用native端
            _nativeChoosePhoneContact(param);
        }
    }

    //下面是一些私有函数
    /**
     * Native端实现选择联系人,并异步返回结果
     * @param  {[type]} param [description]
     * @return {[type]}       [description]
     */
    function _nativeChoosePhoneContact(param) {
        var callbackFunction = param.completion;
        if (callbackFunction != undefined && callbackFunction != null && typeof(callbackFunction) === "function") {
            param.completion = callbackFunction.toString();
        }
        //js -> oc 
        window.webkit.messageHandlers.nativeChoosePhoneContact.postMessage(param);
    }

    //闭包,把Api对象返回
    return NativeApi;
})();

/*
//选择联系人
DANativeApi.choosePhoneContact({
    completion: function(res) {
        alert("选择联系人的结果为:" + JSON.stringify(res));
    }
});
 */

Html中调用

<div>
     <a href="javascript:void(0);" onclick="chooseContact()">选择联系人</a>
     <div id="contactInfo"></div>
</div>
 <script>
       function chooseContact() {
           DANativeApi.choosePhoneContact({
                    completion: function(res) {
                        contactInfo.innerHTML = JSON.stringify(res);
                    }
            });
         }
</script>

WKWebView实现

  • 添加脚本并注册回调
/**
 添加native端的api
 */
- (void)addNativeApiToJS
{
    //防止频繁IO操作,造成性能影响
    static NSString *nativejsSource;
    static dispatch_once_t onceToken;
    dispatch_once(&onceToken, ^{
        nativejsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"NativeApi" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
    });
    //添加自定义的脚本
    WKUserScript *js = [[WKUserScript alloc] initWithSource:nativejsSource injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    [self.webView.configuration.userContentController addUserScript:js];
    //注册回调
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeChoosePhoneContact"];
}
  • WKScriptMessageHandler回调代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"nativeChoosePhoneContact"]) {
        NSLog(@"正在选择联系人");
        [self selectContactCompletion:^(NSString *name, NSString *phone) {
            NSLog(@"选择完成");
            //读取js function的字符串
            NSString *jsFunctionString = message.body[@"completion"];
            //拼接调用该方法的js字符串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)({name: '%@', mobile: '%@'});", jsFunctionString, name, phone];
            //执行回调
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                
            }];
        }];
    }
}
  • 获取联系方式的oc实现
//<CNContactPickerDelegate>
//@property (nonatomic, copy) void(^completion)(NSString *name, NSString *phone);
#pragma mark 选择联系人
- (void)selectContactCompletion:(void(^)(NSString *name, NSString *phone))completion
{
    self.completion = completion;
    CNContactPickerViewController *picker = [[CNContactPickerViewController alloc] init];
    picker.delegate = self;
    picker.displayedPropertyKeys = @[CNContactPhoneNumbersKey];
    [self presentViewController:picker animated:YES completion:^{
        
    }];
}

#pragma mark - CNContactPickerDelegate
- (void)contactPicker:(CNContactPickerViewController *)picker didSelectContactProperty:(CNContactProperty *)contactProperty
{
    if (![contactProperty.key isEqualToString:CNContactPhoneNumbersKey]) {
        return;
    }
    
    CNContact *contact = contactProperty.contact;
    NSString *name = [CNContactFormatter stringFromContact:contact style:CNContactFormatterStyleFullName];
    
    CNPhoneNumber *phoneNumber = contactProperty.value;
    NSString *phone = phoneNumber.stringValue.length ? phoneNumber.stringValue : @"";
    
    //可以把-、+86、空格这些过滤掉
    NSString *phoneStr = [phone stringByReplacingOccurrencesOfString:@"-" withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@"+86" withString:@""];
    phoneStr = [phoneStr stringByReplacingOccurrencesOfString:@" " withString:@""];
    phoneStr = [[phoneStr componentsSeparatedByCharactersInSet:[[NSCharacterSet characterSetWithCharactersInString:@"0123456789"] invertedSet]] componentsJoinedByString:@""];
    
    //回调
    if (self.completion) {
        self.completion(name, phoneStr);
    }
    
    //dissMiss
    [picker dismissViewControllerAnimated:YES completion:nil];
}

- (void)contactPickerDidCancel:(CNContactPickerViewController *)picker
{
    [picker dismissViewControllerAnimated:YES completion:nil];
}

  • 移除注册的回调
- (void)dealloc
{
    //记得移除    
 [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeChoosePhoneContact"];
}

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 202,802评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,109评论 2 379
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 149,683评论 0 335
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,458评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,452评论 5 364
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,505评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,901评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,550评论 0 256
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,763评论 1 296
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,556评论 2 319
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,629评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,330评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,898评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,897评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,140评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,807评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,339评论 2 342

推荐阅读更多精彩内容