WKWebview:与JS交互数据传值、Cookies的注入与清

作者: XueYongWei | 来源:发表于2018-01-15 18:24 被阅读705次

    基本应用

    首先使用WKWebView.你需要导入WebKit。关于WKWebView其他基础使用不在本篇研究范围。
    博主不才,本文根据实际做项目过程中所运用到的进行总结。有不足及错误的地方,还请在评论里指教。本文中所列代码及论述的方法,都在实际项目中应用过的,此项目包含完整的浏览器功能,运用到的模块有:网页进入看图模式、长按链接选择新窗口打开、网页夜间模式、115离线下载、网页原生互相跳转、本公司网页免登录、经验号网页免登录点赞收藏、获取经验网页上文章作者用户信息等。

    目录索引

    本文内容稍杂,由于简书不支持锚点,可以通过复制目录摘要进行搜索,找到自己想要看的内容。
    本篇讨论WKWebview的综合使用,包括以下几个部分:

    • JS交互与数据传递。
      • iOS端执行JS
        • iOS端执行JS代码字符串
        • iOS端执行网页JS方法
      • JS执行iOS端方法
        • 通过iOS原生API执行
        • 跨平台通用方法执行
      • iOS向网页注入JS
      • 网页向iOS传值
      • iOS向网页传值
        • 通过cookies传值
        • 通过JS方法传值
        • 通过window变量传值
    • cookies的注入与清除。
      • cookies的注入
        • 注入cookies
        • 主动使用cookies
      • cookies的清除
        • 清除全部cookies
        • 清除某域名下的cookies
        • iOS9之前版本清除cookies

    ======== WKWebview ========

    一. JS交互与数据传递

    我们使用WKWebview进行更复杂的用法时,难免遇到需要和JS进行交互的情况,方法的相互调用,数据的传递等。

    1. iOS端执行JS
    • 直接执行JS代码

    执行JS方法,使用evaluateJavaScript方法即可,一行代码搞定:

    //OC代码
    [webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable response, NSError * _Nullable error) {
    }];
    
    //swift代码
    webview.evaluateJavaScript(jsStr) { (response, error) in
     }
    

    比如执行JS代码获取网页的标题:

    webView.evaluateJavaScript("document.title") { (jsstr, error) in
                    if let t = jsstr as? String{
                        self.title = t
                    }
                }
    
    • 执行网页的JS方法
      当然,你还有一个使用场景,想要原生某些控件去调用网页里已有的某个js方法。比如:某个网页里有点击到达顶部,或者显示网页章节目录等js方法,我们针对这个网页在原生空间做特殊处理:点击原生toolBar上的目录按钮,让网页显示出章节目录。我们只需要直接执行这个js方法即可。
      已知js的显示章节目录的js方法名为toggleCatalog,代码如下:
    //OC代码
    NSString *jsStr  = @"window.toggleCatalog();";
    [self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable, NSError * _Nullable error) {
            
     }]
    
    //swift代码
    let js ="window.toggleCatalog();" 
    webView?.evaluateJavaScript(js, completionHandler: nil)
    

    一般使用以上方法,是在网页加载完成(didFinishNavigationv)的时候进行操作.

    2. JS执行iOS端方法
    • 通过iOS原生API执行
      JS执行iOS端方法,调用
      window.webkit.messageHandlers.<对象名>.postMessage(<数据>)
      方法,上方代码在JS端写会报错,导致页面后面业务不执行。可使用try-catch执行。
      当然,JS调用的方法,必须是和客户端约定好的,在iOS中的处理方法是:
    - (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
    

    它是WKScriptMessageHandler的代理方法.name和上方JS中的对象名相对应.

    • 跨平台通用方法执行
      在实际应用中,因为h5的跨平台因素,为了和其他平台js方法使用保持一致,我们最好自己写个纯粹的js方法,在方法里调用iOS下的window.webkit.messageHandlers.<对象名>.postMessage(<数据>)

    比如JS需要APP打开用户登录,我们写一个JS方法:

    //我们往网页里注入的js代码
    function callLogin() {
           // APPJS是我们所注入的对象
           window.webkit.messageHandlers.APPJS.postMessage("shouldLogin");
    }
    

    这样注入网页后,网页JS只需调用callLogin()即可唤起原生的方法。
    原生代码里处理方式如下:

    //OC代码
    - (void)userContentController:(WKUserContentController *)userContentController
          didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([message.name isEqualToString:@"APPJS"]) {
            NSLog(@"%@", message.body);
        }
    }
    

    一般的,我们可以将需要调用的方法写入一个js文件,然后注入到网页,网页就可以直接调用这个js方法。

    2. iOS向网页注入JS

    通过使用userContentController向网页注入JS,注入的JS可以取名字,将会在WKScriptMessageHandler的代理方法didReceiveScriptMessage中被回掉。
    注入的String的JS代码,简单的直接写,复杂的可写到一个JS文件里,然后读取文本,创建WKUserScript。

    //OC代码
    NSString *js = @"I am JS Code";
    //初始化WKUserScript对象
    //WKUserScriptInjectionTimeAtDocumentEnd为网页加载完成时注入
    WKUserScript *script = [[WKUserScript alloc] initWithSource:js injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
    //根据生成的WKUserScript对象,初始化WKWebViewConfiguration
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    [config.userContentController addUserScript:script];
    //设置ScriptMessageHandler为self
    [config.userContentController addScriptMessageHandler:self name:@"APPJS"];
    self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
    

    通过从js文件里读取js代码,然后注入网页。

    //swift代码
    //从js文件加载js代码
    let path = (bundlePath) + ("/" + "Contents/Resources/ContextMenu.js")
    let source = try! NSString(contentsOfFile: path, encoding: String.Encoding.utf8.rawValue) as String
                
    let path2 = (bundlePath) + ("/" + "Contents/Resources/JSBridge.js")
    let source2 = try! NSString(contentsOfFile: path2, encoding: String.Encoding.utf8.rawValue) as String
    let js = source + source2
                
    let userScript = WKUserScript(source: js, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
     configuration!.userContentController.addUserScript(userScript)
      //设置ScriptMessageHandler为self
     configuration.userContentController.add(TabManager.sharedInstance, name: "APPJS")
     let newWebView = WKWebView(frame: CGRect.zero, configuration: configuration)
     self.webView = newWebView
    
    3. 网页向iOS传值

    刚才我们已经试过了最简单的放法,但是我们可能需要向原生传值,比如网页加载一片文章完毕,需要告诉原生app,我网页上的一些用户信息。
    我们约定了一个方法叫setPageInfo(info),info是json类型。

    //复杂点的js方法,参数中约定好格式。
    //比如:fun代表方法名,arg代表参数
    function setPageInfo(info) {
           // APPJS是我们所注入的对象
           window.webkit.messageHandlers.APPJS.postMessage({
            fun: 'setPageInfo',
            arg: {
              pageInfo: {
                userID:10086,
                userName:'中国联通',
                isFav:false,
               }
            }
       });
    }
    

    网页只需要调用此方法,我们原生就能得到网页给我们的数据,iOS端处理如下。

    //OC代码
    - (void)userContentController:(WKUserContentController *)userContentController
          didReceiveScriptMessage:(WKScriptMessage *)message {
        if ([message.name isEqualToString:@"APPJS"]) {
            // 打印所传过来的参数,只支持NSNumber, NSString, NSDate, NSArray,
            // NSDictionary, and NSNull类型
            //这里message.body就是JS传过来的info,按字典取值即可。判断fun和args以特殊处理。
            NSLog(@"%@", message.body);
        }else if ([message.name isEqualToString:@"AppModel"]){
            NSLog(@"%@", message.body);
        }
    }
    

    在swift下我们接收、处理数据。

    //swift代码,这个对应上面比较复杂js的处理,按约定格式
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage){
         if message.name == "APPJS" {
                if let dic = message.body as? NSDictionary, dic["fun"] != nil,
                    let fun = (dic["fun"] as AnyObject).description{
                    if let arg = dic["arg"] as? NSArray {
                        if fun == "setPageInfo" {
                            //去取数据即可。
                        }
                  }
              }
    }
    
    3. iOS向网页传值
    • 通过cookies传值
      查看第二部分:二. cookies的注入与清除。
      我们通过设置cookies,然后与网页开发者约定,让网页从cookies里取值即可。(比如用户的authKey)
    • 通过JS方法传值
      实际上,在我们使用JS调用原生方法的时候,应该就会有这个疑问:如果获得返回值?是的,暂时没办法获得返回值。
      不过我们可以曲线救国。
      比如我们需要获得app的用户信息:
     getUserInfoString: function () {
      window.webkit.messageHandlers.OOFJS.postMessage({
        fun: 'getUserInfoString',
        arg: {
        callback: 'setJSUserInfo'
        }
      })
    },
    

    注意到我们的arg参数里,多了个参数叫callback: 'setJSUserInfo',那么我们就知道我们收集完用户信息,怎么告诉JS用户信息是什么。
    我们只需要通过第一步的方法,执行这个JS方法,并传入用户信息即可。

    let infoStr = getUserInfoJsonStr()
    let jsStr =  "setJSUserInfo(infoStr)"
    webView?.evaluateJavaScript(, completionHandler: nil)
    

    然后JS的这个方法就被回调了,JS可以在得到用户信息后,做进一步的数据更新。

    • 通过window变量传值
      通过第二步我们已经能正确的让JS拿到我们iOS里面的信息,但是我们发现不能及时地拿到返回值会有很大的限制,比如有这种情况,JS需要直接拿到返回值进行其他操作:
    //JS代码
    var info = getUserInfoString()
    if info....
    

    这就GG了,因为JS调用这个方法的时候,我们没办法直接给它返回。
    我们再次曲线救国。
    我们可以在调用这个方法之前,比如网页加载完毕,网页收到相应等情况下,提前准备好数据,然后写入到window里!把infoString写入window的方法:

    let infoStr = getUserInfoJsonStr()
    let jsStr =  "window.GLOBAL_USERINFO=\(infoStr)"
    webView.evaluateJavaScript(jsStr, completionHandler: nil)
    

    执行完毕后,window就有了一个GLOBAL_USERINFO变量,随时可取。不过网页开发者不必知道这个东西,无需为了iOS而特殊处理,仍让他调用getUserInfoString方法即可。
    然后我们改写这个方法,让它能直接返回infoStr:

     getUserInfoString: function () {
      window.webkit.messageHandlers.OOFJS.postMessage({
        fun: 'getUserInfoString',
        arg: {
        }
      })
      return window.GLOBAL_USERINFO;
    },
    

    这个方法是直接返回了我们提前写入的变量window.GLOBAL_USERINFO。

    二. cookies的注入与清除。

    WKWebView会忽视默认的网络存储, NSURLCache, NSHTTPCookieStorage, NSCredentialStorage。 目前是这样的,WKWebView有自己的进程,同样也有自己的存储空间用来存储cookie和cache, 其他的网络类如NSURLConnection是无法访问到的。 同时WKWebView发起的资源请求也是不经过NSURLProtocol的,导致无法自定义请求。

    WKWebView与UIWebview的一个区别,就是WKWebView实例将会忽略任何的默认网络存储器(NSURLCache, NSHTTPCookieStorage, NSCredentialStorage) 和一些标准的自定义网络请求类(NSURLProtocol,等等.).
    WKWebView实例不会把Cookie存入到App标准的的Cookie容器(NSHTTPCookieStorage)中,因为 NSURLSession/NSURLConnection等网络请求使用NSHTTPCookieStorage进行访问Cookie,所以不能访问WKWebView的Cookie,现象就是WKWebView存了Cookie,其他的网络类如NSURLSession/NSURLConnection却看不到。
    与Cookie相同的情况就是WKWebView的缓存,凭据等。WKWebView都拥有自己的私有存储,因此和标准cocoa网络类兼容的不是那么好

    NSHTTPCookieStorage 实现管理cookie的单利,每个cookie都是NSHTTPCookie类的实例,做为一个规则,cookie在所有应用 之间共享并在不同进程之间保持同步。
    上面引入了网页需要用户登陆,然后让app跳转登陆界面进行登陆,app登陆之后自然要向网页注入cookie,来让网页继续剩下的功能。

    1. 在webview发起请求的时候附带cookie。

    这个适用首次发起网页请求,同样适用点击,在webview代理方法里,判断是否需要注入cookie的域名,如果是,截断请求,重新发起注入了cookie的请求。

    //oc代码
    NSMutableDictionary *cookieDic = [NSMutableDictionary dictionary];
    NSMutableString *cookieValue = [NSMutableString stringWithFormat:@""];
    NSHTTPCookieStorage *cookieJar = [NSHTTPCookieStorage sharedHTTPCookieStorage];
    for (NSHTTPCookie *cookie in [cookieJar cookies]) {
         [cookieDic setObject:cookie.value forKey:cookie.name];
    }
        
     // cookie重复,先放到字典进行去重,再进行拼接
    for (NSString *key in cookieDic) {
          NSString *appendString = [NSString stringWithFormat:@"%@=%@;", key, [cookieDic valueForKey:key]];
          [cookieValue appendString:appendString];
    }
    NSMutableURLRequest * request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:self.url]];
    [request addValue:cookieValue forHTTPHeaderField:@"Cookie"];
    NSLog(@"添加cookie");
    [self.webView loadRequest:request];
    
    //swift代码
    guard let cookies = HTTPCookieStorage.shared.cookies else {
                return
     }
    var cookieDic = Dictionary<String, Any>()
    var cookieValue = ""
    for cookie in  cookies{
       cookieDic[cookie.name] = cookie.value
    }
    for (key,value) in cookieDic {
       let appendString = "\(key)=\(value)"
       cookieValue.append(appendString)
    }
     let request = URLRequest.init(url: URL.init(string: "url")!)
     request.addValue(cookieValue, forHTTPHeaderField: "Cookie")
    
    2. 在webview创建的时候js注入cookie。

    其中js的写法问题,有可能有多个写法是cookie之间用隔开。

    //OC代码
    WKUserContentController* userContentController = WKUserContentController.new;
    
    WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource: @"document.cookie ='TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    
    [userContentController addUserScript:cookieScript];
    
    WKWebViewConfiguration* webViewConfig = WKWebViewConfiguration.new;
    
    webViewConfig.userContentController = userContentController;
    
    WKWebView * webView = [[WKWebView alloc] initWithFrame:CGRectMake(/*set your values*/) configuration:webViewConfig];
    
    //swift代码
    let userContent = WKUserContentController()
    let jsStr = "document.cookie ='TeskCookieKey1=TeskCookieValue1';document.cookie = 'TeskCookieKey2=TeskCookieValue2';"
            
    let cookieScript = WKUserScript.init(source: jsStr, injectionTime: WKUserScriptInjectionTime.atDocumentStart, forMainFrameOnly: false)
    userContent.addUserScript(cookieScript)
    let webViewConfig = WKWebViewConfiguration()
    webViewConfig.userContentController = userContent
            
    let webview = WKWebView.init(frame: CGRect(x: 0, y: 0, width: 300, height: 300), configuration: webViewConfig)
    
    3. 在webview加载内容时js注入cookie。
    //swift代码
    func webView(_ webView: WKWebView, didCommit navigation: WKNavigation!) {
    if let laToken = UserCenter.shared().user?.laToken {
                    let cookie = "115token=\(oofToken)"
                    webView?.evaluateJavaScript("function setCookie(e,o){document.cookie=e+\"=\"+escape(o)+\";path=/;domain=.115.com\"}for(var cookieTem= \"\(cookie)\",cookieArr=cookieTem.split(\";\"),i=0;i<cookieArr.length;i++){var temArr=cookieArr[i].split(\"=\");setCookie(temArr[0],temArr[1])}", completionHandler: {
                        (object, error) -> Void in
                    })
                }
    }
    

    经过验证,最好是第一点和第三点同时使用,第二点每次截断请求总觉得浪费资源 -。-

    • 主动使用cookies
      通过写入或修改cookies,可以达到与网页开发者商定的数据交流。
    2. cookie清除

    某些情况,需要清理已经注入的cookie,比如浏览器的清理缓存,或者用户退出登录等。需要注意的是WKWebview的清理cookieAPI,是iOS9之后才有的。

    • 清除全部cookies
      WKWebsiteDataStore可根据需要自行选择,具体参数参阅文档与注释。
    let websiteDataTypes = WKWebsiteDataStore.allWebsiteDataTypes()
                
    let dateFrom = Date.init(timeIntervalSince1970: 0)
                
    WKWebsiteDataStore.default().removeData(ofTypes: websiteDataTypes, modifiedSince: dateFrom, completionHandler: {
                })
    

    注意:此操作将会清空cookies,类似浏览器里“清除记录”的功能。

    • 清除某域名下的cookies
      如果是app的用户退出登录,需要清理的仅仅是自己家cookie(即某域名下),则可以这样:
      比如我们现在app里退出登录,需要清理自己用户的的数据,而不是全部cookies,代码如下:
      (displayName是域名,我们这里是只要包含115就清理。)
    WKWebsiteDataStore.default().fetchDataRecords(ofTypes:websiteDataTypes, completionHandler: { (records) in
      for record in records {
        debugPrint("fetch cookies -> \(record)")
        if record.displayName.contains("115") {
          WKWebsiteDataStore.default().removeData(ofTypes: WKWebsiteDataStore.allWebsiteDataTypes(), for: [record], completionHandler: {
          debugPrint("清理了cookies -> \(record)")
          })
        }
       }
    })
    
    • iOS9之前版本清除cookies
      在iOS9之前,wkwebview是没有清理cookie的方法的,所以需要对不同的版本进行不同的操作。
      那iOS9之前的如何操作?可以预见,既然是缓存,肯定是放在沙盒里的。找到沙盒的目录,删除文件即可。
    /// 清理cookie缓存数据
    func ClearCache() {
      if #available(iOS 9.0, *) {
        // 根据需求,通过API清理cookie
      } else {//否则清理文件夹
        let libraryPath = NSSearchPathForDirectoriesInDomains(.libraryDirectory, .userDomainMask, true)[0]
        let cookiesFolderPath = libraryPath+"/Cookies"
        try? FileManager.default.removeItem(atPath: cookiesFolderPath)
      }
    }
    

    相关文章

      网友评论

      • a132de74a5ec:你好,我用"window.GLOBAL_USERINFO=\(infoStr)",这种方法为变量GLOBAL_USERINFO赋值,调用这段js是成功了的。但是为什么从window 里面拿到的GLOBAL_USERINFO的值为空呢
        XueYongWei:infoStr不能为空,或者为空串。如果需要设为空,infoStr应该为"{}"
      • c015f7af94e5:不错不错

      本文标题:WKWebview:与JS交互数据传值、Cookies的注入与清

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