UIWebView & WKWebView 詳解上

前言

一直想系統(tǒng)的總結(jié)下UIWebViewWKWebView泊藕,這里整理了一個(gè)
Demo可供參考

分為兩部分:
UIWebView & WKWebView 上
UIWebView & WKWebView 下

OC-->JS

UIWebView OC-->JS

  • 1、通過調(diào)用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;方法
  • 2宇植、在頁面加載完成后,獲取JSContext上下文埋心,通過JSContext的- (JSValue *)evaluateScript:(NSString *)script;方法得到JSValue對(duì)象,JSValue對(duì)象可轉(zhuǎn)為Array忙上、Number拷呆、String、對(duì)象等數(shù)據(jù)類型

WKWebView OC-->JS

通過調(diào)用方法:- (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疫粥、該方法不能判斷調(diào)用了一個(gè)js方法之后茬斧,是否發(fā)生了錯(cuò)誤。當(dāng)錯(cuò)誤發(fā)生時(shí)梗逮,返回值為nil项秉,而當(dāng)調(diào)用一個(gè)方法本身沒有返回值時(shí),返回值也為nil慷彤,所以無法判斷是否調(diào)用成功了娄蔼。
2怖喻、返回值類型為nullable NSString *,就意味著當(dāng)調(diào)用的js方法有返回值時(shí)岁诉,都以字符串返回锚沸,不夠靈活。當(dāng)返回值是一個(gè)js的Array時(shí)涕癣,還需要解析字符串哗蜈,比較麻煩。

  • - (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調(diào)用js
    JSValue *value = [self.jsContext evaluateScript:@"document.title"];
    self.title = value.toString;
}

1坠韩、 其實(shí)WebKit都有一個(gè)內(nèi)嵌的js環(huán)境距潘,一般我們?cè)陧撁婕虞d完成之后,獲取js上下文只搁,然后通過JSContext的evaluateScript:方法來獲取返回值音比。因?yàn)樵摲椒ǖ玫降氖且粋€(gè)JSValue對(duì)象,所以支持JavaScript的Array须蜗、Number硅确、String、對(duì)象等數(shù)據(jù)類型明肮。該方法解決了stringByEvaluatingJavaScriptFromString:返回值只是NSString的問題菱农。

2、 [self.jsContext evaluateScript:@"document.titlexxxx"];那么必然會(huì)報(bào)錯(cuò)柿估,報(bào)錯(cuò)了循未,可以通過 @property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);,設(shè)置該block來獲取異常

//在調(diào)用前秫舌,設(shè)置異车难回調(diào)
 [self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
     NSLog(@"%@", exception);
 }];
     //執(zhí)行方法
 JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];

該方法,也很好的解決了stringByEvaluatingJavaScriptFromString:調(diào)用js方法后足陨,出現(xiàn)錯(cuò)誤卻捕獲不到的缺點(diǎn)嫂粟。

WKWebView OC-->JS解析

//@property (strong, nonatomic) WKWebView *webView;
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation
{
//    self.title = self.webView.title;
//     //執(zhí)行一段js,并將結(jié)果返回墨缘,如果出錯(cuò)星虹,error則不為空
    [self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        self.title = title;
    }];
}

該方法很好的解決了UIWebView使用stringByEvaluatingJavaScriptFromString:方法的兩個(gè)缺點(diǎn)(1. 返回值只能是NSString。2. 報(bào)錯(cuò)無法捕獲)镊讼。


JS-->0C

UIWebView JS-->0C

1宽涌、攔截URL
OC中,只要遵循了UIWebViewDelegate協(xié)議, 每次打開一個(gè)鏈接之前蝶棋,都會(huì)觸發(fā)方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在該方法中卸亮,捕獲該鏈接,并且返回NO(阻止本次跳轉(zhuǎn))玩裙,從而執(zhí)行對(duì)應(yīng)的OC方法兼贸。

2段直、self.jsContext[@"yourMethodName"] = your block;其中yourMethodName就是js的方法名稱,賦給是一個(gè)block 里面是oc代碼

WKWebView JS-->OC

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

2坷牛、WKUserContentController中新增方法

  • 注冊(cè)回調(diào) - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
  • js中調(diào)用方法 window.webkit.messageHandlers.<name>.postMessage(<messageBody>)
  • oc中將會(huì)收到WKScriptMessageHandler的回調(diào)
    - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
  • 移除
    - (void)removeScriptMessageHandlerForName:(NSString *)name;

攔截URL解析

1、Html中實(shí)現(xiàn)
比如syhcandy://方法是在html或者js中很澄,點(diǎn)擊某個(gè)按鈕觸發(fā)事件時(shí)京闰,跳轉(zhuǎn)到自定義URL Scheme構(gòu)成的鏈接,而Objective-C中捕獲該鏈接甩苛,從中解析必要的參數(shù)蹂楣,實(shí)現(xiàn)JS到OC的一次交互。比如頁面中一個(gè)a標(biāo)簽讯蒲,鏈接如下:

<a href="syhcandy://smsLogin?username=syh&code=776632">短信驗(yàn)證登錄</a>

UIWebView的JS-->OC中URL攔截實(shí)現(xiàn)

而在Objective-C中痊土,只要遵循了UIWebViewDelegate協(xié)議,那么每次打開一個(gè)鏈接之前墨林,都會(huì)觸發(fā)方法- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在該方法中赁酝,捕獲該鏈接,并且返回NO(阻止本次跳轉(zhuǎn))旭等,從而執(zhí)行對(duì)應(yīng)的OC方法酌呆。

#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
    //標(biāo)準(zhǔn)的URL包含scheme、host搔耕、port隙袁、path、query弃榨、fragment等
    NSURL *URL = request.URL;
    if ([URL.scheme isEqualToString:SHWebViewDemoScheme]) {
        if ([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]) {
            NSLog(@"短信驗(yàn)證碼登錄菩收,參數(shù)為 %@", URL.query); //短信驗(yàn)證碼登錄,參數(shù)為 username=syh&code=776632
            return NO;
        }
    }
    return YES;
}

缺點(diǎn):無法直接獲取本次交互的返回值鲸睛,比較適合單向傳參娜饵,且不關(guān)心回調(diào)的情景,比如h5頁面跳轉(zhuǎn)到native頁面等官辈。

WKWebView JS-->OC中URL攔截實(shí)現(xiàn)

當(dāng)用戶點(diǎn)擊這個(gè)a標(biāo)簽時(shí)箱舞,會(huì)被攔截

//針對(duì)一次action來決定是否允許跳轉(zhuǎn),允許與否都需要調(diào)用decisionHandler钧萍,比如decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    //可以通過navigationAction.navigationType獲取跳轉(zhuǎn)類型,如新鏈接政鼠、后退等
    NSURL *URL = navigationAction.request.URL;
    //判斷URL是否符合自定義的URL Scheme
    if([URL.scheme isEqualToString:SHWebViewDemoScheme]){
        //根據(jù)不同的業(yè)務(wù)风瘦,來執(zhí)行對(duì)應(yīng)的操作,且獲取參數(shù)
        if([URL.host isEqualToString:SHWebViewDemoHostSmsLogin]){
            NSString *param = URL.query;
            NSLog(@"短信驗(yàn)證碼登錄, 參數(shù)為%@", param);//短信驗(yàn)證碼登錄, 參數(shù)為username=12323123&code=892845
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
    }
    decisionHandler(WKNavigationActionPolicyAllow);
}

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

<div>
<a href="javascript:void(0);" class="sharebtn" onclick="share('分享標(biāo)題', 'http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg', location.href)">分享活動(dòng)公般,領(lǐng)30元紅包</a>
</div>
<script>
    //簡單分享
    function share (title, imgUrl, link) {
        //便于WKWebView測試
        window.webkit.messageHandlers.share.postMessage({title: title, imgUrl: imgUrl, link: link});
        //這里需要OC實(shí)現(xiàn)
    }
</script>

UIWebView JS-->OC中OC實(shí)現(xiàn)

self.jsContext[@"yourMethodName"] = your block;方法映射
在頁面加載完成時(shí)万搔,先獲取js上下文胡桨。獲取到之后,我們就可以進(jìn)行強(qiáng)大的方法映射了瞬雹。

- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    [self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
    //其中share就是js的方法名稱昧谊,賦給是一個(gè)block 里面是iOS代碼
    //此方法最終將打印出所有接收到的參數(shù),js參數(shù)是不固定的
    self.jsContext[@"share"] = ^(){
        NSArray *args = [JSContext currentArguments];//獲取到share里的所有參數(shù)
        //args中的元素是JSValue酗捌,需要轉(zhuǎn)成OC的對(duì)象
        NSMutableArray *messages = [NSMutableArray array];
        for (JSValue *obj in args) {
            [messages addObject:[obj toObject]];
        }
        NSLog(@"點(diǎn)擊分享js傳回的參數(shù):\n%@", messages);
        /**
         點(diǎn)擊分享js傳回的參數(shù):
         (
         "\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新增兩個(gè)方法如下:

//在OC中添加一個(gè)scriptMessageHandler呢诬,則會(huì)在all frames中添加一個(gè)js的function: window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 。
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
- (void)removeScriptMessageHandlerForName:(NSString *)name;

WKWebView JS-->OC中OC實(shí)現(xiàn)

  • 在OC中添加一個(gè)handler
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注冊(cè)回調(diào)
 [UserContentController addScriptMessageHandler:self name:@"share"];
  • js調(diào)用share方法后胖缤,OC會(huì)收到WKScriptMessageHandler回調(diào)
#pragma mark - WKScriptMessageHandler  js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"share"]) {
        id body = message.body;
        NSLog(@"share分享的內(nèi)容為:%@", body);
        /**
         share分享的內(nèi)容為:{
         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";
         }
         */
    }
}
  • 記得移除注冊(cè)的回調(diào)
- (void)dealloc
{
    //記得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
}

需求:寫一個(gè)有兩個(gè)參數(shù)尚镰,一個(gè)返回值的js方法,oc應(yīng)該怎么替換呢哪廓?

Html中

<div onclick="alert(testAddMethod(1,5))">點(diǎn)擊測試兩數(shù)相加</div>
<script>
    //該方法傳入兩個(gè)整數(shù)狗唉,求和,并返回結(jié)果
    function testAddMethod (a, b) {
        //需要OC實(shí)現(xiàn)a+b涡真,并返回
        return a + b;
    }
</script>

UIWebView中對(duì)應(yīng)的OC替換該方法的實(shí)現(xiàn):

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

需求升級(jí):調(diào)用方法原實(shí)現(xiàn)分俯,并且在原結(jié)果上乘以10

//調(diào)用方法的本來實(shí)現(xiàn),給原結(jié)果乘以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中有一個(gè)分享按鈕哆料,用戶點(diǎn)擊之后缸剪,調(diào)用native分享(微信分享、微博分享等)剧劝,在native分享成功或者失敗時(shí)橄登,回調(diào)h5頁面,告訴其分享結(jié)果讥此,h5頁面刷新對(duì)應(yīng)的UI拢锹,顯示分享成功或者失敗。

Html的代碼

<a href="javascript:void(0);" onclick="test()">測試新分享</a></br>
<h>下面展示分享結(jié)果</p><div id="shareResult"></div>
<script>
/**
 * 分享方法萄喳,并且會(huì)異步回調(diào)分享結(jié)果
 * @param  {對(duì)象類型} shareData 一個(gè)分享數(shù)據(jù)的對(duì)象卒稳,包含title,imgUrl,link以及一個(gè)回調(diào)function
 * @return {void}     無同步返回值
 js的shareNew方法的參數(shù)是一個(gè)對(duì)象,該對(duì)象包含了幾個(gè)必要的字段他巨,以及一個(gè)回調(diào)函數(shù)充坑,這個(gè)回調(diào)函數(shù)有點(diǎn)像oc的block,調(diào)用者把一個(gè)function傳入一個(gè)function當(dāng)作參數(shù)染突,在適當(dāng)時(shí)候捻爷,方法內(nèi)實(shí)現(xiàn)者調(diào)用該function,實(shí)現(xiàn)對(duì)調(diào)用者的異步回調(diào)份企。
 */
function shareNew(shareData) {
    var title = shareData.title;
    var imgUrl = shareData.imgUrl;
    var link = shareData.link;
    var result = shareData.result;
    //do something
    //這里模擬異步操作
    setTimeout(function(){
        //2s之后也榄,回調(diào)true分享成功
        result(true);
    }, 2000);     
    //用于WKWebView,因?yàn)閃KWebView并沒有辦法把js function傳遞過去,因此需要特殊處理一下
    //把js function轉(zhuǎn)換為字符串甜紫,oc端調(diào)用時(shí) (<js function string>)(true); 即可
    shareData.result = result.toString();
    window.webkit.messageHandlers.shareNew.postMessage(shareData);
}

function test() {
    //清空分享結(jié)果
    shareResult.innerHTML = "";
    
    //調(diào)用時(shí)降宅,應(yīng)該
    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代碼解析:

點(diǎn)擊之后,觸發(fā)了test()函數(shù)囚霸,test()中封裝了對(duì)shareNew()函數(shù)的調(diào)用腰根,且傳了一個(gè)對(duì)象作為參數(shù),對(duì)象中result字段對(duì)應(yīng)的是個(gè)匿名函數(shù)拓型,緊接著shareNew()函數(shù)調(diào)用额嘿,其中的實(shí)現(xiàn)是2s過后,result(true);模擬js異步實(shí)現(xiàn)異步回調(diào)結(jié)果吨述,分享成功岩睁。同時(shí)shareNew()函數(shù)中,因?yàn)橥ㄟ^scriptMessageHandler無法傳遞function揣云,所以先把shareData對(duì)象中的result這個(gè)匿名function轉(zhuǎn)成String捕儒,然后替換shareData對(duì)象的result屬性為這個(gè)String,并回傳給OC邓夕,OC這邊對(duì)應(yīng)JS對(duì)象的數(shù)據(jù)類型是NSDictionary刘莹,我們打印并得到了所有參數(shù),同時(shí)焚刚,把result字段對(duì)應(yīng)的js function String取出來点弯。這里我們延遲4s回調(diào),模擬Native分享的異步過程矿咕,在4s后抢肛,也就是js中顯示success的2s過后,調(diào)用js的匿名function碳柱,并傳遞參數(shù)(分享結(jié)果)捡絮。調(diào)用一個(gè)js function的方法是 functionName(argument);,這里由于這個(gè)js的function已經(jīng)是一個(gè)String了莲镣,所以我們調(diào)用時(shí)福稳,需要加上(),如 (functionString)(argument);因此瑞侮,最終我們通過OC -> JS 的evaluateJavaScript:completionHandler:方法的圆,成功完成了異步回調(diào),并傳遞給js一個(gè)分享失敗的結(jié)果半火。

上面的描述看起來很復(fù)雜越妈,其實(shí)就是先執(zhí)行了JS的默認(rèn)實(shí)現(xiàn),后執(zhí)行了OC的實(shí)現(xiàn)钮糖。上面的代碼展示了如何解決scriptMessageHandler的兩個(gè)問題梅掠,并且實(shí)現(xiàn)了一個(gè) JS -> OC、OC -> JS 完整的交互流程。

UIWebView在OC中的實(shí)現(xiàn):

- (void)convertJSFunctionsToOCMethods
{
        self.jsContext[@"shareNew"] = ^(JSValue *shareData){//首先這里要注意瓤檐,回調(diào)的參數(shù)不能直接寫NSDictionary類型,為何呢娱节?
        //仔細(xì)看挠蛉,打印出的確實(shí)是一個(gè)NSDictionary,但是result字段對(duì)應(yīng)的不是block而是一個(gè)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對(duì)象的result屬性肄满,這個(gè)JSValue對(duì)應(yīng)的其實(shí)是一個(gè)javascript的function谴古。
        JSValue *resultFunction = [shareData valueForProperty:@"result"];
        //回調(diào)block,將js的function轉(zhuǎn)換為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中的實(shí)現(xiàn)

  • 注冊(cè)回調(diào)
//WKUserContentController *UserContentController = [[WKUserContentController alloc] init];
//注冊(cè)回調(diào)
[UserContentController addScriptMessageHandler:self name:@"shareNew"];
  • WKScriptMessageHandler代理調(diào)用
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
     if ([message.name isEqualToString:@"shareNew"]){
        NSDictionary *shareData = message.body;
        NSLog(@"%@分享的數(shù)據(jù)為: %@", message.name, shareData);
        /**
         shareNew分享的數(shù)據(jù)為: {
         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;
         }
         */
        
        //模擬異步回調(diào)
        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";
             
             }
             */
            //拼接調(diào)用該方法的js字符串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO];    //后面的參數(shù)NO為模擬分享失敗
            //執(zhí)行回調(diào)
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                if (!error) {
                    NSLog(@"模擬回調(diào)稠歉,分享失敗");
                }
            }];
        });
    }
}
  • 移除注冊(cè)
- (void)dealloc
{
    //記得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"shareNew"];
}
Native預(yù)覽H5頁面中的image

場景:在微信中瀏覽網(wǎng)頁時(shí)掰担,看到喜歡的圖片,你會(huì)點(diǎn)擊圖片查看大圖怒炸,然后長按圖片保存带饱。

分析

1、如果想在Native預(yù)覽H5中的image阅羹,最需要的是什么勺疼?是圖片的鏈接。如果能有縮略圖更好了捏鱼。
2执庐、只要獲取了鏈接,就可以跳轉(zhuǎn)到一個(gè)ViewController中导梆,預(yù)覽圖片轨淌,后續(xù)長按保存自然水到渠成。
3看尼、那應(yīng)該如何獲取圖片的鏈接呢递鹉?通過JS -> OC 傳遞圖片url

方案

當(dāng)頁面加載完成后,給html頁面中所有無默認(rèn)點(diǎn)擊事件的<img>添加點(diǎn)擊事件狡忙,當(dāng)用戶點(diǎn)擊時(shí)梳虽,拿到所有參數(shù)。
(其實(shí)這不是最好的方案灾茁,最好的解決方案是窜觉,跟前端約定一下,哪些圖片需要預(yù)覽北专,哪些img標(biāo)簽的id統(tǒng)一禀挫,或者有個(gè)特定的屬性,這樣客戶端可以根據(jù)id找到這些img標(biāo)簽)

Html中有img標(biāo)簽

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

寫好一個(gè)ImgAddClickEvent.js文件拓颓,來實(shí)現(xiàn)給所有無默認(rèn)點(diǎn)擊事件的<img>添加點(diǎn)擊事件语婴。

//獲取所有img標(biāo)簽
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')) {
        //添加到圖片鏈接數(shù)組中
        imgUrls.push(img.src || img.getAttribute('data-src'));
        //如果圖片沒有默認(rèn)的onclick事件,且父元素不是a標(biāo)簽,則添加onclick事件砰左,當(dāng)用戶點(diǎn)擊時(shí)匿醒,把圖片鏈接回傳給Native
        if (!img.onclick && img.parentElement.tagName !== "A") {
            //給圖片添加下標(biāo)的屬性
            img.index = i;      //記錄下標(biāo)
            //添加點(diǎn)擊事件,并且回傳選中的圖片鏈接缠导、下標(biāo)廉羔、屏幕上的位置、全部的圖片數(shù)組等
            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實(shí)現(xiàn)

UIWebView直接使用JavaScriptCore<img>添加onclick方法為OC的實(shí)現(xiàn)即可僻造。

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

    /*
     Native預(yù)覽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];
    });
    //先注入給圖片添加點(diǎn)擊事件的js
    [self.jsContext evaluateScript:jsSource];
    self.jsContext[@"h5ImageDidClick"] = ^(NSDictionary *imgInfo){
        NSLog(@"UIWebView點(diǎn)擊了html上的圖片,信息是:%@", imgInfo);
    };
    /**
     UIWebView點(diǎn)擊了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實(shí)現(xiàn)

添加腳本

/**
 Native預(yù)覽H5頁面中的image,
 頁面中的所有img標(biāo)簽添加點(diǎn)擊事件
 */
- (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方法揪罕,那么你本地可能就會(huì)有一個(gè)ImgAddClickEvent.js
     */
    //添加自定義的腳本
    WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    [self.webView.configuration.userContentController addUserScript:js];
    //注冊(cè)回調(diào)
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];

}

注冊(cè)回調(diào)后的代理方法

#pragma mark - WKScriptMessageHandler  js -> oc
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"imageDidClick"]) {
        //點(diǎn)擊了html上的圖片, Native預(yù)覽H5頁面中的image
        NSLog(@"點(diǎn)擊了html上的圖片宝泵,參數(shù)為%@", message.body);
        /**
         點(diǎn)擊了html上的圖片耸序,參數(shù)為{
         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(@"點(diǎn)擊了第%@個(gè)圖片坎怪,\n鏈接為%@,\n在Screen中的絕對(duì)frame為%@廓握,\n所有的圖片數(shù)組為%@", @(index), selectedImageUrl, NSStringFromCGRect(frame), dict[@"imgUrls"]);
        /*
         點(diǎn)擊了第0個(gè)圖片搅窿,
         鏈接為http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg,
         在Screen中的絕對(duì)frame為{{8, 72}, {252, 168}}隙券,
         所有的圖片數(shù)組為(
         "http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg"
         )
         */

    }
    
}

移除

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

Native為H5提供一套Native Api(微信男应、支付寶小程序)

很多時(shí)候,Native與H5交互得深了娱仔,必定會(huì)有一些更深層次的需求沐飘。比如h5想控制頁面的pop、push牲迫、present耐朴,想調(diào)用Native的Share,想調(diào)用Native的掃描二維碼功能盹憎,獲取掃描結(jié)果……

微信提供的一些Api(掃碼筛峭、選擇照片等)都是如何實(shí)現(xiàn)的呢?很明顯陪每,native提供的影晓。

首先镰吵,我們?yōu)镠5提供一套Api,那自然Api是暴露給js的挂签,所以這些Api也是js的疤祭。下面針對(duì)一些需求,分析下封裝和實(shí)現(xiàn)饵婆。其中用到了js閉包画株,需要一點(diǎn)js知識(shí)。

從通訊錄選擇聯(lián)系人

從通訊錄選擇聯(lián)系人的例子啦辐,從js到native,再從native到j(luò)s蜈项。

js端實(shí)現(xiàn)

/**
 * Native為H5提供的Api接口
 *
 * @type {js對(duì)象}
 */
var DANativeApi = (function() {

    var NativeApi = {
        /**
         * 從通訊錄選擇聯(lián)系人
         * @return {void} 無同步返回值芹关,異步返回選擇的結(jié)果
         */
        choosePhoneContact: function(param) {
            //具體是否需要判斷
            //調(diào)用native端
            _nativeChoosePhoneContact(param);
        }
    }

    //下面是一些私有函數(shù)
    /**
     * Native端實(shí)現(xiàn)選擇聯(lián)系人,并異步返回結(jié)果
     * @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對(duì)象返回
    return NativeApi;
})();

/*
//選擇聯(lián)系人
DANativeApi.choosePhoneContact({
    completion: function(res) {
        alert("選擇聯(lián)系人的結(jié)果為:" + JSON.stringify(res));
    }
});
 */

Html中調(diào)用

<div>
     <a href="javascript:void(0);" onclick="chooseContact()">選擇聯(lián)系人</a>
     <div id="contactInfo"></div>
</div>
 <script>
       function chooseContact() {
           DANativeApi.choosePhoneContact({
                    completion: function(res) {
                        contactInfo.innerHTML = JSON.stringify(res);
                    }
            });
         }
</script>

WKWebView實(shí)現(xiàn)

  • 添加腳本并注冊(cè)回調(diào)
/**
 添加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];
    //注冊(cè)回調(diào)
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeChoosePhoneContact"];
}
  • WKScriptMessageHandler回調(diào)代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
    if ([message.name isEqualToString:@"nativeChoosePhoneContact"]) {
        NSLog(@"正在選擇聯(lián)系人");
        [self selectContactCompletion:^(NSString *name, NSString *phone) {
            NSLog(@"選擇完成");
            //讀取js function的字符串
            NSString *jsFunctionString = message.body[@"completion"];
            //拼接調(diào)用該方法的js字符串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)({name: '%@', mobile: '%@'});", jsFunctionString, name, phone];
            //執(zhí)行回調(diào)
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                
            }];
        }];
    }
}
  • 獲取聯(lián)系方式的oc實(shí)現(xiàn)
//<CNContactPickerDelegate>
//@property (nonatomic, copy) void(^completion)(NSString *name, NSString *phone);
#pragma mark 選擇聯(lián)系人
- (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:@""];
    
    //回調(diào)
    if (self.completion) {
        self.completion(name, phoneStr);
    }
    
    //dissMiss
    [picker dismissViewControllerAnimated:YES completion:nil];
}

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

  • 移除注冊(cè)的回調(diào)
- (void)dealloc
{
    //記得移除    
 [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeChoosePhoneContact"];
}

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末轴总,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子博个,更是在濱河造成了極大的恐慌怀樟,老刑警劉巖,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件盆佣,死亡現(xiàn)場離奇詭異往堡,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)共耍,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門虑灰,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人痹兜,你說我怎么就攤上這事穆咐。” “怎么了字旭?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵对湃,是天一觀的道長。 經(jīng)常有香客問我遗淳,道長熟尉,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任洲脂,我火速辦了婚禮斤儿,結(jié)果婚禮上剧包,老公的妹妹穿的比我還像新娘。我一直安慰自己往果,他們只是感情好疆液,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著陕贮,像睡著了一般堕油。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上肮之,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天掉缺,我揣著相機(jī)與錄音,去河邊找鬼戈擒。 笑死眶明,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的筐高。 我是一名探鬼主播搜囱,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢(mèng)啊……” “哼柑土!你這毒婦竟也來了蜀肘?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬榮一對(duì)情侶失蹤稽屏,失蹤者是張志新(化名)和其女友劉穎扮宠,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狐榔,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡涵卵,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了荒叼。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片轿偎。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖被廓,靈堂內(nèi)的尸體忽然破棺而出坏晦,到底是詐尸還是另有隱情,我是刑警寧澤嫁乘,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布昆婿,位于F島的核電站,受9級(jí)特大地震影響蜓斧,放射性物質(zhì)發(fā)生泄漏仓蛆。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一挎春、第九天 我趴在偏房一處隱蔽的房頂上張望看疙。 院中可真熱鬧豆拨,春花似錦、人聲如沸能庆。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽搁胆。三九已至弥搞,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間渠旁,已是汗流浹背攀例。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留顾腊,地道東北人粤铭。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓,卻偏偏與公主長得像投慈,于是被迫代替她去往敵國和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子冠骄,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容