Dark Angel

iOS中UIWebView与WKWebView、JavaScript与OC交互、Cookie管理看我就够(下)

次阅读
本文目录
  1. 前言
  2. Safari调试
    1. 开启Safari开发菜单
    2. iPhone开启Web检查器
    3. 运行App
    4. 调试对应的页面
  3. 与前端配合解决bug
  4. 实际应用中一些需求的实现
    1. 自定义浏览器UserAgent
    2. Native与H5共享登录状态
    3. Native预览H5页面中的image
      1. 分析
      2. 方案
      3. UIWebView实现
      4. WKWebView实现
      5. 注意
    4. Native加载并缓存H5页面中的img
    5. Native分享H5页面到微信、QQ等
    6. Native为H5提供一套Native Api(微信、支付宝小程序)
      1. 分享
      2. 从通讯录选择联系人
      3. 扫描二维码
  5. 总结

前言

在前面的文章中,我们介绍了UIWebViewWKWebView一些使用,与JS的交互和一些坑,相信看过的小伙伴们,已经大概清楚了吧,如果有问题,欢迎提问。

本文是本系列文章的最后一篇,主要为小伙伴们分享下Safari调试与前端的配合以及实际应用中一些需求的实现等

关于文中提到的一些内容,这里我准备了个Demo,有需要的小伙伴可以下载。

Safari调试

在前面的文章中,查看网页的Cookie,其实已经用到了Safari调试。笔者觉得Safari调试功能真的很有用,通过它可以轻松定位问题的所在。也因此,公司中App一旦有问题出现,不管是客户端的问题,还是前端的问题,找问题的重任都落到了笔者的身上呢。这一度是一个困扰😪。想象一下,h5页面的一个bug,App端帮忙快速定位,并且告知h5相关开发人员该如何修复,是多么伟大的一件事情。

下面来简单讲讲怎么用Safari调试。

开启Safari开发菜单

在Mac的Safari偏好设置中,开启开发菜单。具体步骤为:Safari -> 偏好设置… -> 高级 -> 勾选在菜单栏显示“开发”菜单

B49417E2-433A-4650-8BF7-AF941E8B6670

iPhone开启Web检查器

具体步骤为:设置 -> Safari -> 高级 -> Web 检查器

8AECBA1E-FF4A-4D7F-8D74-E224035EA418

运行App

打开项目,Cmd + R 运行,打开想调试的Web页面。

调试对应的页面

打开Safari -> 开发 -> 设备 -> URL。

5B046813-A658-45B4-9ED2-A1480BA5EE99

选中的页面会变成蓝色,点击然后打开了如下的界面。

693A55F1-ADD7-4D84-B8D1-4B669D3CF561

这个页面就很像Windows 平台ChromeF12。可以打断点:

7A23FD0A-40A9-48D9-B53E-B721F455C1D5

查看断点

E2ABC34F-D227-4A71-AC0C-7C3AC9B730F9

查看Cookie

0E90DD8A-B178-4EF3-BF8C-F90C6E3A278F

打印Cookie或者元素
834DB431-32B4-44EC-BED2-05E3D124FFBA

比如我在这里Alert页面的title,输入 alert(document.title);,你会在模拟器中看到弹窗

550D157D-0F2E-41E1-B5E3-85928FFCBCAC

整体十分有用,操作的体验跟Xcode很像,小伙伴们自行探索。

与前端配合解决bug

前端有一些问题,在浏览器中是无法调试的,很可能只在App内的浏览器中才会复现。这个时候你可以期待前端开发人员会使用XcodeSafari调试来解决bug,或者靠自己。毕竟大家的目标一致,给用户提供一个更好的App,解决所有已知问题。

这里我举个例子,运用Safari调试来解决一个前端的bug。

比如新做的h5页面中,有一个分享按钮,点击调用原生的分享,但是发现,点击之后没有反应了,什么问题呢?是Native端实现有问题,还是前端写的有问题呢?如图

我们来帮忙看下吧,打开Safari Web 检查器,定位到资源,并且在share方法中添加断点,如图

76A2E384-0A07-4DA0-8D5C-61DB988C4AE0

会发现,并没有断住,而是页面直接报错了,仔细查看错误描述,share方法里多了一个“/”,因此报错了。当我点击分享按钮时6792A001-7ACC-4336-BF1E-6A03983C74E1会发现,提示找不到变量share。这里我需要说明一下:

当js中报错的时候,报错位置所在的函数以及报错位置之后的代码,都不会执行,所以我点击分享时,提示的是找不到方法,因为js的语法不对,报错了,这里解析不出来,所以也就没有了sharetestAddMethod和之后的函数。

那么当我点击分享下面的按钮是,调用share下面定义的方法也就会提示找不到对应的函数了。

至此,问题找到了,只要告之前端开发人员即可,让他修复即可。

实际遇到的问题可能要复杂的多,可以通过断点,以及控制台打印一些js变量的值,DOM操作来寻找问题,解决问题。希望可以帮助到小伙伴们。

实际应用中一些需求的实现

自定义浏览器UserAgent

这个其实在App开发中,比较重要。比如常见的微信、支付宝App等,都有自己的UserAgent,而UA最常用来判断在哪个App内,一般App的下载页中只有一个按钮”点击下载”,当用户点击该按钮时,在微信中则跳转到应用宝,否则跳转到AppStore。那么如何区分在哪个App中呢?就是js判断UA。

//js中判断
if (navigator.userAgent.indexOf("MicroMessenger") !== -1) {
   //在微信中
}

关于自定义UA,这个UIWebView不提供Api,而WKWebView提供Api,前文中也说明过,就是调用customUserAgent属性。

self.webView.customUserAgent = @"WebViewDemo/1.0.0";    //自定义UA,只支持WKWebView

而有没有其他的方法实现自定义浏览器UserAgent呢?有。

//最好在AppDelegate中就提前设置
@implementation AppDelegate


- (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
    // Override point for customization after application launch.

    //设置自定义UserAgent
    [self setCustomUserAgent];
    return YES;
}

- (void)setCustomUserAgent
{
    //get the original user-agent of webview
    UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
    NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];
    //add my info to the new agent
    NSString *newAgent = [oldAgent stringByAppendingFormat:@" %@", @"WebViewDemo/1.0.0"];
    //regist the new agent
    NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", newAgent, @"User-Agent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];
}

@end

上面的代码,展示了在原有UserAgent的基础上,添加一些自定义的内容。

6F9FAD1A-DBD6-4FA6-89CC-28921D57646E

可以看到原本的UA后面已经有我们添加的内容了WebViewDemo/1.0.0

这里需要说明的是:

  1. 通过NSUserDefaults设置自定义UserAgent,可以同时作用于UIWebViewWKWebView
  2. WKWebViewcustomUserAgent属性,优先级高于NSUserDefaults,当同时设置时,显示customUserAgent的值。如上图。

Native与H5共享登录状态

这个需求在前面的文章中针对WKWebViewUIWebView分别单独做过介绍。维持登录状态,依赖的是相同的Cookie

UIWebView实现起来基本不需要做额外的操作,只要保证sharedHTTPCookieStorage中的Cookie是没问题的。

WKWebView实现起来相对麻烦,有很多坑,这里不再详细描述,小伙伴们可以看下上篇文章中Cookie管理一节。

Native预览H5页面中的image

这个需求,应该是一个比较常见的需求。在微信中浏览网页时,看到喜欢的图片,你会点击图片查看大图,然后长按图片保存。

分析

如果你的项目中有这样的需求的话,可能你需要做如下的分析。

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

这里,究竟如何实现获取图片链接,取决于你用的是UIWebView还是WKWebView

方案

当页面加载完成后,给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);
}

下面分别介绍UIWebViewWKWebView如何实现。

UIWebView实现

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

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    [self convertJSFunctionsToOCMethods];
}

- (void)convertJSFunctionsToOCMethods {
    //获取该UIWebview的javascript上下文
    //self持有jsContext
    //@property (nonatomic, strong) JSContext *jsContext;
    self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

      //先注入给图片添加点击事件的js
    //防止频繁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];
    });
    [self.jsContext evaluateScript:jsSource];
    //替换回调方法
    self.jsContext[@"h5ImageDidClick"] = ^(NSDictionary *imgInfo) {
        NSLog(@"UIWebView点击了html上的图片,信息是:%@", imgInfo);
    };
}

WKWebView实现

WKWebView实现,需要使用WKUserScriptscriptMessageHandler,下面简单介绍下,详细实现,见Demo。

WKWebViewUIViewController中实现如下

/**
 页面中的所有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];
    });
    //添加自定义的脚本
    WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
    [self.webView.configuration.userContentController addUserScript:js];
    //注册回调
    [self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"imageDidClick"]) {
        //点击了html上的图片
        NSLog(@"点击了html上的图片,参数为%@", message.body);
    }
}

当我点击这个标签时,因为我添加了onclick事件,在OC端我会接收到回调。因此打印出log

0B6DFD21-5707-419E-9918-5AA6792D0CAD

注意

上面无论是UIWebView还是WKWebView,参数中的x,y是不包含自定义scrollView的contentInset的,如果要获取图片在手机屏幕上的位置:

 x = x + self.webView.scrollView.contentInset.left;
 y = y + self.webView.scrollView.contentInset.top;

拿到了这些信息,想必,你可以实现一个十分完美的图片预览效果了。

Native加载并缓存H5页面中的img

因为页面中的img标签加载图片的网络请求是由WebView管理的,所以要想Native接管图片的下载,只有2条路:

  1. 在页面加载前,把页面中img标签的src换成一张native的占位图或者””,并且把img.src传递到native,在native下载图片或者读取缓存完毕后,再把相应的img标签的src设置成本地的,如img.src =”native cache url”。整体交互是JS->Native, Native -> JS。
  2. NSURLProtocol拦截WebView的所有图片请求,交由我们自己管理。

比较有可行性的是方法2。但也只限于UIWebViewWKWebView上篇文中说过,虽然有私有Api,但是笔者不推荐使用。

先说下,为何方法1不可行。首先,页面加载前,是在什么时候呢?如果h5不做修改,全部交由Native端处理,是没有办法修改html的,因为UIWebViewWKWebView都没有提供一个Api,在图片加载之前告诉你html是什么内容,所以这个方法是走不通的。除非你用loadHTMLString的方法加载,加载前先替换img的src。但是loadHTMLString的方法,又加载不到Web端的js和css,只能用于本地拼接完整HTML String的情况,不适用于一般的场景。So,这条路是走不通的,局限性太大了。

方法2的核心思路就是拦截请求,最核心的是在你的NSURLProtocol子类中,实现这个方法

+ (BOOL)canInitWithRequest:(NSURLRequest *)request {
    //处理过不再处理
    if ([NSURLProtocol propertyForKey:DAURLProtocolHandledKey inRequest:request]) {
        return NO;
    }
    //根据request header中的 accept 来判断是否加载图片
    /*
    {
     "Accept" = "image/png,image/svg+xml,image/*;q=0.8,*\/*;q=0.5\";
     "User-Agent" = "Mozilla/5.0 (iPhone; CPU iPhone OS 10_3 like Mac OS X) AppleWebKit/603.1.30 (KHTML, like Gecko) Mobile/14E269 WebViewDemo/1.0.0";
    }
     */
    NSDictionary *headers = request.allHTTPHeaderFields;
    NSString *accept = headers[@"Accept"];
    if (accept.length >= @"image".length && [accept rangeOfString:@"image"].location != NSNotFound) {
        return YES;
    }
    return NO;
}

当拦截到图片请求时,再做后续的处理,下面写一些伪代码

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request {
    return request;
}
+ (BOOL)requestIsCacheEquivalent:(NSURLRequest *)a toRequest:(NSURLRequest *)b {
    return [super requestIsCacheEquivalent:a toRequest:b];
}
- (void)startLoading {
    NSMutableURLRequest *mutableReqeust = [[self request] mutableCopy];
    //这里也可以添加一些自定义的header,看具体需求
    //标记该request已经处理过
    [NSURLProtocol setProperty:@(YES) forKey:DAURLProtocolHandledKey inRequest:mutableReqeust];

    //这里NSURLProtocolClient的相关方法都要调用
    //比如 [self.client URLProtocol:self didLoadData:data];
    .....
}
- (void)stopLoading {
    .....
}

这部分代码,Demo中并没有完全实现,如果小伙伴们有兴趣研究下具体的实现,可以参考这篇文章, 笔者这里就不多说了。

Native分享H5页面到微信、QQ等

这个需求,其实我在前面的文章中针对UIWebViewWKWebView都已经做了很详细的介绍。这里,简单分享下如何获取分享的内容。

一般分享到微信或者QQ至少需要的参数是

  1. Title(主标题)
  2. Description(副标题或者描述)
  3. ThumbnailImage(缩略图)
  4. WebpageUrl(h5页面的链接)

这些参数,都怎么获取呢?通过OC->JS的方式,获取。当然你也可以自定义。

var title = document.title;
var desc = document.getElementsByTagName("article")[0].textContent;     //或者 document.body.innerText; 或者  document.getElementById("yourId").innerText; 
var thumbnailImageUrl = document.getElementsByTagName("img")[0].src;    //或者看需求取哪个
var webpageUrl = location.href;

具体如何用OC调用JS获取这些值,这里就不多说了,看过前面文章的小伙伴,可以自行实现。

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

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

那么近半年比较🔥的小程序,微信提供的一些Api(扫码、选择照片等)都是如何实现的呢?很明显,native提供的。

笔者作为支付宝小程序(尚未发布)的内测用户之一,也接触了支付宝小程序,其中也有很多Api是native提供的。

其实这就涉及到一个完整的Native与JS交互的流程,从JS->Native到Native->JS。也就是前面介绍过的,异步回调结果。这个不局限于iOS,Android也是同样的。

首先,我们为H5提供一套Api,那自然Api是暴露给js的,所以这些Api也是js的。笔者封装了一个接口文件:NativeApi.js(在最新的Demo中有)。下面针对一些需求,分析下封装和实现。其中用到了js闭包,需要一点js知识。

分享

前面的文章中,笔者最常举的一个例子就是分享,Native为H5提供原生的分享方法,h5调用后可以获取分享结果(成功or失败)。这里针对分享这个如何实现,就不赘述了。笔者直接贴上适用于WKWebViewjs代码。

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

    var NativeApi = {
        /**
         * 分享
         * @param  {js对象} shareInfo 分享信息和回调
         * @return {void}           无同步返回值,异步返回分享结果 true or false
         */
        share: function(shareInfo) {
            if (shareInfo == undefined || shareInfo == null || typeof(shareInfo) !== "object") {
                alert("参数" + JSON.stringify(shareInfo) + "不合法");
            } else {
                alert("分享的参数为" + JSON.stringify(shareInfo));
            }
            //调用native端
            _nativeShare(shareInfo);
        }
    }

    //下面是一些私有函数
    /**
     * Native端实现,适用于WKWebView,UIWebView如何实现,小伙伴自己动脑筋吧~
     * @param  {js对象} shareInfo 分享的信息和回调
     * @return {void}           无同步返回值,异步返回
     */
    function _nativeShare(shareInfo) {
        //用于WKWebView,因为WKWebView并没有办法把js function传递过去,因此需要特殊处理一下
        //把js function转换为字符串,oc端调用时 (<js function string>)(true); 即可
        //如果有回调函数,且为function
        var callbackFunction = shareInfo.result;
        if (callbackFunction != undefined && callbackFunction != null && typeof(callbackFunction) === "function") {
            shareInfo.result = callbackFunction.toString();
        }
        //js -> oc 
        // 至于Android端,也可以,比如 window.jsInterface.nativeShare(JSON.stringify(shareInfo));
        window.webkit.messageHandlers.nativeShare.postMessage(shareInfo);
    }

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

/*

//调用时,分享
DANativeApi.share({
    title: document.title,
    desc: "",
    url: location.href,
    imgUrl: "",
    result: function(res) {
        // body...
        alert("分享结果为:" + JSON.stringify(res));
    }
});

 */

Native端不贴了,小伙伴们看Demo吧。

从通讯录选择联系人

这里笔者再举个从通讯录选择联系人的例子,从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));
    }
});
 */

OC端依然加载此文件,并注册handler

/**
 添加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"];
}

#pragma mark - WKScriptMessageHandler  js -> oc

- (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) {

            }];
        }];
    }
}

具体回调方式,在之前的WKWebView中,讲过了,这里不再赘述,选择联系人用的是Contacts框架,具体的小伙伴可以看Demo。

整体效果如下:

new

扫描二维码

相信看过从通讯录选择联系人的实现,小伙伴们可以自行实现扫描二维码了吧~ 快动手尝试一下吧~

总结

本文给小伙伴们介绍了下Safari调试,以及其具体运用,并且分享了实际应用中一些需求的实现方式。

结合前面的两篇文章, 相信现在小伙伴们一定对WebView有相当深刻的理解了吧。那么,本系列文章也告一段落了,具体有问题的话,欢迎提问。

Dark Angel

iOS界的低调探索者

 
Powered By Hexo