OC WKWebView的JS與OC互動、Cookie管理

韋家冰發表於2017-12-13

完全抄錄:iOS中UIWebView與WKWebView、JavaScript與OC互動、Cookie管理看我就夠(中)

####WKWebView 是Apple於iOS 8.0推出的WebKit中的核心控制元件,用來替UIWebView。 WKWebView比UIWebView的優勢在於:

1、更多的支援HTML5的特性 2、高達60fps的滾動重新整理率以及內建手勢 3、與Safari相同的JavaScript引擎 4、將UIWebViewDelegate與UIWebView拆分成了14類與3個協議(官方文件說明) 5、可以獲取載入進度:estimatedProgress(UIWebView需要呼叫私有Api)

WKUserContentController

@property (nonatomic, strong) WKUserContentController *userContentController;
複製程式碼
@interface WKUserContentController : NSObject <NSCoding>
//讀取新增過的指令碼
@property (nonatomic, readonly, copy) NSArray<WKUserScript *> *userScripts;
//新增指令碼
- (void)addUserScript:(WKUserScript *)userScript;
//刪除所有新增的指令碼
- (void)removeAllUserScripts;
//通過window.webkit.messageHandlers.<name>.postMessage(<messageBody>) 來實現js->oc傳遞訊息,並新增handler
- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;
//刪除handler
- (void)removeScriptMessageHandlerForName:(NSString *)name;
@end
複製程式碼

建立一個WKWebView的程式碼如下:

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKUserContentController *controller = [[WKUserContentController alloc] init];
configuration.userContentController = controller;
self.webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.webView.allowsBackForwardNavigationGestures = YES; //允許右滑返回上個連結,左滑前進
self.webView.allowsLinkPreview = YES; //允許連結3D Touch
self.webView.customUserAgent = @"WebViewDemo/1.0.0"; //自定義UA,UIWebView就沒有此功能,後面會講到通過其他方式實現
self.webView.UIDelegate = self;
self.webView.navigationDelegate = self;
[self.view addSubview:self.webView];
複製程式碼

一些屬性

@property (nullable, nonatomic, readonly, copy) NSString *title;    //頁面的title,終於可以直接獲取了
@property (nullable, nonatomic, readonly, copy) NSURL *URL;     //當前webView的URL
@property (nonatomic, readonly, getter=isLoading) BOOL loading; //是否正在載入
@property (nonatomic, readonly) double estimatedProgress;   //載入的進度
@property (nonatomic, readonly) BOOL canGoBack; //是否可以後退,跟UIWebView相同
@property (nonatomic, readonly) BOOL canGoForward;  //是否可以前進,跟UIWebView相同

複製程式碼

#####動態注入JS

給每個頁面新增一個Cookie

//注入一個Cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@"document.cookie = 'DarkAngelCookie=DarkAngel;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
[controller addUserScript:newCookieScript];

複製程式碼

然後再注入一個指令碼,每當頁面載入,就會alert當前頁面cookie,在OC中的實現

//建立指令碼
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:@"alert(document.cookie);" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
//新增指令碼
[controller addUserScript:script];
複製程式碼

#####OC -> JS

[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
        NSLog(@"呼叫evaluateJavaScript非同步獲取title:%@", title);
}];
複製程式碼

#####JS -> OC 1、URL攔截

- (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:@"darkangel"]) {
        //根據不同的業務,來執行對應的操作,且獲取引數
        if ([URL.host isEqualToString:@"smsLogin"]) {
            NSString *param = URL.query;
            NSLog(@"簡訊驗證碼登入, 引數為%@", param);
            decisionHandler(WKNavigationActionPolicyCancel);
            return;
        }
    }
    decisionHandler(WKNavigationActionPolicyAllow);
    NSLog(@"%@", NSStringFromSelector(_cmd));
}
複製程式碼

2、WKScriptMessageHandler OC中新增一個WKScriptMessageHandler,則會在all frames中新增一個js的function

(1)OC中監聽JS的方法

// 監聽JS的currentCookies方法
[controller addScriptMessageHandler:self name:@"currentCookies"]; //這裡self要遵循協 WKScriptMessageHandler
複製程式碼

(2)JS中呼叫下面的方法時

// JS呼叫currentCookies的時候寫上這句程式碼
window.webkit.messageHandlers.currentCookies.postMessage(document.cookie);
複製程式碼

(3)OC中將會收到WKScriptMessageHandler的回撥

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"currentCookies"]) {
        NSString *cookiesStr = message.body;    //message.body返回的是一個id型別的物件,所以可以支援很多種js的引數型別(js的function除外)
        NSLog(@"當前的cookie為: %@", cookiesStr);
    }
}
複製程式碼

當然,記得在適當的地方呼叫removeScriptMessageHandler

- (void)dealloc {
    //記得移除
    [self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"currentCookies"];
}
複製程式碼

######實現JS -> OC的呼叫,並且OC -> JS 非同步回撥結果,這裡還是拿分享來舉個例子

JS

/**
   * 分享方法,並且會非同步回撥分享結果
   * @param  {物件型別} shareData 一個分享資料的物件,包含title,imgUrl,link以及一個回撥function
   * @return {void}  無同步返回值
   */
  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";
        }
    });
  }
複製程式碼

在html頁面中我定義了一個a標籤來觸發test()函式

<a href="javascript:void(0);" onclick="test()">測試新分享</a>
複製程式碼

在OC端,實現如下

//首先別忘了,在configuration中的userContentController中新增scriptMessageHandler
[controller addScriptMessageHandler:self name:@"shareNew"]; //記得適當時候remove哦


//點選a標籤時,則會呼叫下面的方法
#pragma mark - WKScriptMessageHandler 

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"shareNew"]) {
        NSDictionary *shareData = message.body;
        NSLog(@"shareNew分享的資料為: %@", shareData);
        //模擬非同步回撥
        dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(4 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
            //讀取js function的字串
            NSString *jsFunctionString = shareData[@"result"];
            //拼接呼叫該方法的js字串
            NSString *callbackJs = [NSString stringWithFormat:@"(%@)(%d);", jsFunctionString, NO];    //後面的引數NO為模擬分享失敗
            //執行回撥
            [self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
                if (!error) {
                    NSLog(@"模擬回撥,分享失敗");
                }
            }];
        });
    }
}
複製程式碼

那麼當我點選a標籤時,html頁面上過2s,會顯示success,然後再過2s,會顯示failure。 ####Cookie管理

    // 獲取儲存的cookies
    NSArray *cookies = [PSNetworkDataHelper cookiesForURL:url];
    
    // 每次都設定request的HTTPHeaderField
    NSDictionary *cookiesDict = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    [request setValue:[cookiesDict objectForKey:@"Cookie"] forHTTPHeaderField:@"Cookie"];
    
    
    // 每次都JS注入
    NSMutableString *cookiesString = [NSMutableString string];
    for (NSHTTPCookie *cookie in cookies) {
        NSString *string = [NSString stringWithFormat:@"%@=%@; ",cookie.name,cookie.value];
        [cookiesString appendFormat:@"document.cookie='%@';", string];
    }
    WKUserScript * cookieScript = [[WKUserScript alloc]
                                   initWithSource: cookiesString
                                   injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
    
    [self.wkWebView.configuration.userContentController addUserScript:[self setCookie]];
    
    [self.wkWebView loadRequest:request];
複製程式碼

#####使用WKWebView需要注意的地方 ######Cookie問題 ######JS彈窗,需要全部現實WKUIDelegate的代理

 #pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler();
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
    
}
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:@"提示" message:message?:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:([UIAlertAction actionWithTitle:@"取消" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(NO);
    }])];
    [alertController addAction:([UIAlertAction actionWithTitle:@"確認" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(YES);
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable))completionHandler{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:prompt message:@"" preferredStyle:UIAlertControllerStyleAlert];
    [alertController addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {
        textField.text = defaultText;
    }];
    [alertController addAction:([UIAlertAction actionWithTitle:@"完成" style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {
        completionHandler(alertController.textFields[0].text?:@"");
    }])];
    [self presentViewController:alertController animated:YES completion:nil];
}
複製程式碼

######白屏問題 當WKWebView載入的網頁佔用記憶體過大時,會出現白屏現象。

解決:
A、藉助 WKNavigtionDelegate: (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView API_AVAILABLE(macosx(10.11), ios(9.0));回撥函式:
當 WKWebView 總體記憶體佔用過大,頁面即將白屏的時候,系統會呼叫上面的回撥函式,我們在該函式裡執行[webView reload](這個時候 webView.URL 取值尚不為 nil)解決白屏問題。在一些高記憶體消耗的頁面可能會頻繁重新整理當前頁面,H5側也要做相應的適配操作。
B、檢測 webView.title 是否為空:在WKWebView白屏的時候,另一種現象是 webView.titile 會被置空, 因此,可以在 viewWillAppear 的時候檢測 webView.title 是否為空來 reload 頁面
複製程式碼

######導航欄半透明問題,能不用半透明就不用了,很折騰

相關文章