WKWebView詳解&WKWebVieW和JS互動

小輝哥的掘金發表於2019-03-04

開發App的過程中,常常會遇到在App內部載入網頁,通常用UIWebView載入。而這個自iOS2.0開始使用的Web容器一直是開發的心病:載入速度慢,佔用記憶體多,優化困難。如果載入網頁多,還可能因為過量佔用記憶體而給系統kill掉。各種優化的方法效果也不那麼明顯iOS8 以後,蘋果推出了新框架 WebKit,提供了替換 UIWebView 的元件 WKWebView。各種 UIWebView 的效能問題沒有了,速度更快了,佔用記憶體少了,體驗更好了,下面列舉一些其它的優勢:
1、在效能、穩定性、功能方面有很大提升(最直觀的體現就是載入網頁是佔用的記憶體,模擬器載入百度與開源中國網站時,WKWebView佔用23M,而UIWebView佔用85M);
2、允許JavaScript的Nitro庫載入並使用(UIWebView中限制);
3、支援了更多的HTML5特性;
4、高達60fps的滾動重新整理率以及內建手勢;
5、將UIWebViewDelegate與UIWebView重構成了14類與3個協議(檢視蘋果官方文件);

14個類

WKBackForwardList: 之前訪問過的 web 頁面的列表,可以通過後退和前進動作來訪問到。
WKBackForwardListItem: webview 中後退列表裡的某一個網頁。
WKFrameInfo: 包含一個網頁的佈局資訊。
WKNavigation: 包含一個網頁的載入進度資訊。
WKNavigationAction: 包含可能讓網頁導航變化的資訊,用於判斷是否做出導航變化。
WKNavigationResponse: 包含可能讓網頁導航變化的返回內容資訊,用於判斷是否做出導航變化。
WKPreferences: 概括一個 webview 的偏好設定。
WKProcessPool: 表示一個 web 內容載入池。
WKUserContentController: 提供使用 JavaScript post 資訊和注射 script 的方法。
WKScriptMessage: 包含網頁發出的資訊。
WKUserScript: 表示可以被網頁接受的使用者指令碼。
WKWebViewConfiguration: 初始化 webview 的設定。
WKWindowFeatures: 指定載入新網頁時的視窗屬性。

3個協議

WKNavigationDelegate: 提供了追蹤主視窗網頁載入過程和判斷主視窗和子視窗是否進行頁面載入新頁面的相關方法。
WKUIDelegate: 提供用原生控制元件顯示網頁的方法回撥。
WKScriptMessageHandler: 提供從網頁中收訊息的回撥方法。

所有相關的類的API

//上文介紹過的偏好配置
@property (nonatomic, readonly, copy) WKWebViewConfiguration *configuration;
// 導航代理 
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// 使用者互動代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;

// 頁面前進、後退列表
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;

// 預設構造器
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;


//載入請求API
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;

// 載入URL
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL allowingReadAccessToURL:(NSURL *)readAccessURL NS_AVAILABLE(10_11, 9_0);

// 直接載入HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;

// 直接載入data
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL NS_AVAILABLE(10_11, 9_0);

// 前進或者後退到某一頁面
- (nullable WKNavigation *)goToBackForwardListItem:(WKBackForwardListItem *)item;

// 頁面的標題,支援KVO的
@property (nullable, nonatomic, readonly, copy) NSString *title;

// 當前請求的URL,支援KVO的
@property (nullable, nonatomic, readonly, copy) NSURL *URL;

// 標識當前是否正在載入內容中,支援KVO的
@property (nonatomic, readonly, getter=isLoading) BOOL loading;

// 當前載入的進度,範圍為[0, 1]
@property (nonatomic, readonly) double estimatedProgress;

// 標識頁面中的所有資源是否通過安全加密連線來載入,支援KVO的
@property (nonatomic, readonly) BOOL hasOnlySecureContent;

// 當前導航的證照鏈,支援KVO
@property (nonatomic, readonly, copy) NSArray *certificateChain NS_AVAILABLE(10_11, 9_0);

// 是否可以招待goback操作,它是支援KVO的
@property (nonatomic, readonly) BOOL canGoBack;

// 是否可以執行gofarward操作,支援KVO
@property (nonatomic, readonly) BOOL canGoForward;

// 返回上一頁面,如果不能返回,則什麼也不幹
- (nullable WKNavigation *)goBack;

// 進入下一頁面,如果不能前進,則什麼也不幹
- (nullable WKNavigation *)goForward;

// 重新載入頁面
- (nullable WKNavigation *)reload;

// 重新從原始URL載入
- (nullable WKNavigation *)reloadFromOrigin;

// 停止載入資料
- (void)stopLoading;

// 執行JS程式碼
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

// 標識是否支援左、右swipe手勢是否可以前進、後退
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;

// 自定義user agent,如果沒有則為nil
@property (nullable, nonatomic, copy) NSString *customUserAgent NS_AVAILABLE(10_11, 9_0);

// 在iOS上預設為NO,標識不允許連結預覽
@property (nonatomic) BOOL allowsLinkPreview NS_AVAILABLE(10_11, 9_0);

#if TARGET_OS_IPHONE
/*! @abstract The scroll view associated with the web view.
 */
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
#endif

#if !TARGET_OS_IPHONE
// 標識是否支援放大手勢,預設為NO
@property (nonatomic) BOOL allowsMagnification;

// 放大因子,預設為1
@property (nonatomic) CGFloat magnification;

// 根據設定的縮放因子來縮放頁面,並居中顯示結果在指定的點
- (void)setMagnification:(CGFloat)magnification centeredAtPoint:(CGPoint)point;

複製程式碼

#使用
####1、首先需要先引入WebKit庫

#import <WebKit/WebKit.h>
複製程式碼

####2、兩種初始化方法

// 預設初始化
- (instancetype)initWithFrame:(CGRect)frame;

// 根據對webview的相關配置,進行初始化
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
複製程式碼

3、載入網頁

最基礎的方法和UIWebView一樣

NSURL *url = [NSURL URLWithString:@"www.jianshu.com"];
NSURLRequest *request = [NSURLRequest requestWithURL:url];
[webView loadRequest:request];
複製程式碼

一些其他的載入方法

//載入本地URL檔案
- (nullable WKNavigation *)loadFileURL:(NSURL *)URL 
               allowingReadAccessToURL:(NSURL *)readAccessURL

//載入本地HTML字串
- (nullable WKNavigation *)loadHTMLString:(NSString *)string
                                  baseURL:(nullable NSURL *)baseURL;
//載入二進位制資料
- (nullable WKNavigation *)loadData:(NSData *)data MIMEType:(NSString *)MIMEType characterEncodingName:(NSString *)characterEncodingName baseURL:(NSURL *)baseURL
複製程式碼

4、代理方法

1、【WKNavigationDelegate協議】

該代理提供的方法,可以用來追蹤載入過程(頁面開始載入、載入完成、載入失敗)、決定是否執行跳轉。

// 頁面開始載入時呼叫
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation;
// 當內容開始返回時呼叫
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation;
// 頁面載入完成之後呼叫
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation;
// 頁面載入失敗時呼叫
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation;
複製程式碼

頁面跳轉的代理方法有三種,分為(收到跳轉與決定是否跳轉兩種)

// 接收到伺服器跳轉請求之後呼叫
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation;
// 在收到響應後,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
// 在傳送請求之前,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
複製程式碼
2、【WKUIDelegate協議】

WKUIDelegate從名稱能看出它是webView在user interface上的代理,共有5個可選型別的代理方法。它為webView提供了原生的彈框,而不是JavaScript裡的提示框。雖然JavaScript的提示框可以做的跟原生一樣,但是對於ios開發者來說,如果要更改提示框就不方便了。提供這個代理,可以讓ios端更加靈活的修改提示框的樣式。

// 新建WKWebView
- (nullable WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction *)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures;

// 關閉WKWebView
- (void)webViewDidClose:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);

// 對應js的Alert方法
/**
 *  web介面中有彈出警告框時呼叫
 *
 *  @param webView           實現該代理的webview
 *  @param message           警告框中的內容
 *  @param frame             主視窗
 *  @param completionHandler 警告框消失呼叫
 */
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;

// 對應js的confirm方法
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler;

// 對應js的prompt方法
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * __nullable result))completionHandler;
複製程式碼
3、【WKScriptMessageHandler】

這個協議中包含一個必須實現的方法,這個方法是native與web端互動的關鍵,它可以直接將接收到的JS指令碼轉為OC或Swift物件。

// 從web介面中接收到一個指令碼時呼叫
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
複製程式碼

重點之WKWebVieW和JS互動

HTML程式碼

<html>
    <!--描述網頁資訊-->
    <head>
		<meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
        <title>title</title>
 		<style>
            *{
                font-size: 50px;
            }
        
            .btn{height:80px; width:80%; padding: 0px 30px; background-color: #0071E7; border: solid 1px #0071E7; border-radius:5px; font-size: 1em; color: white}
 		</style>
        
        <script>
            
        
            //OC呼叫JS的方法列表
        	function alertMobile() {
                document.getElementById(`mobile`).innerHTML = `不帶引數`
        	}

        	function alertName(msg) {
                //有一個引數
                document.getElementById(`name`).innerHTML = `有一個引數 :` + msg
        	}

        	function alertSendMsg(num,msg) {
        		//有兩個引數
                document.getElementById(`msg`).innerHTML = `有兩個引數:` + num + `,` + msg + `!!`
            }
        
            //JS響應方法列表
            function btnClick1() {
                window.webkit.messageHandlers.showMobile.postMessage(null)
            }

            function btnClick2() {
                window.webkit.messageHandlers.showName.postMessage(`有一個引數`)
            }

            function btnClick3() {
                window.webkit.messageHandlers.showSendMsg.postMessage([`兩個引數One`, `兩個引數Two`])
            }

        </script>
        
        
    </head>

    <!--網頁具體內容-->
    <body>
        <br/>

		<div>
			<label>WKWebView&JS互動</label>
		</div>
		<br/>

        <div id="mobile"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick1()">不帶引數</button>
		</div>
		<br/>
        
        <div id="name"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick2()">一個引數</button>
		</div>
		<br/>
        
        <div id="msg"></div>
		<div>
			<button class="btn" type="button" onclick="btnClick3()">兩個引數</button>
		</div>


    </body>
</html>

複製程式碼

OC程式碼

需要的標頭檔案

#import <WebKit/WebKit.h>
複製程式碼

需要遵守的代理

1、設定偏好設定,以及JS呼叫OC 新增處理指令碼,這裡的內容我寫在了viewDidLoad裡面,但是需要注意的是,需要在我們結束的時候釋放WKUserContentController,不然會造成記憶體洩漏

- (void)viewDidLoad {
    [super viewDidLoad];
    // 設定偏好設定
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    // 預設為0
    config.preferences.minimumFontSize = 10;
    //是否支援JavaScript
    config.preferences.javaScriptEnabled = YES;
    //不通過使用者互動,是否可以開啟視窗
    config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
    
    self.webView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, self.view.bounds.size.width, self.view.bounds.size.height/2) configuration:config];
    [self.view addSubview:self.webView];

    
    NSString *filePath = [[NSBundle mainBundle] pathForResource:@"index" ofType:@"html"];
    NSURL *baseURL = [[NSBundle mainBundle] bundleURL];
    [self.webView loadHTMLString:[NSString stringWithContentsOfFile:filePath encoding:NSUTF8StringEncoding error:nil] baseURL:baseURL];
    
    WKUserContentController *userCC = config.userContentController;
    //JS呼叫OC 新增處理指令碼
    [userCC addScriptMessageHandler:self name:@"showMobile"];
    [userCC addScriptMessageHandler:self name:@"showName"];
    [userCC addScriptMessageHandler:self name:@"showSendMsg"];
    
}
複製程式碼

2、釋放WKUserContentController程式碼

-(void)removeAllScriptMsgHandle{
    WKUserContentController *controller = self.webView.configuration.userContentController;
    [controller removeScriptMessageHandlerForName:@"showMobile"];
    [controller removeScriptMessageHandlerForName:@"showName"];
    [controller removeScriptMessageHandlerForName:@"showSendMsg"];
}
複製程式碼

在JS呼叫OC以後會走的代理

#pragma mark - WKScriptMessageHandler

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    NSLog(@"%@",NSStringFromSelector(_cmd));
    NSLog(@"%@",message.body);
    
    if ([message.name isEqualToString:@"showMobile"]) {
        [self showMsg:@"沒有引數"];
    }
    
    if ([message.name isEqualToString:@"showName"]) {
        NSString *info = [NSString stringWithFormat:@"%@",message.body];
        [self showMsg:info];
    }
    
    if ([message.name isEqualToString:@"showSendMsg"]) {
        NSArray *array = message.body;
        NSString *info = [NSString stringWithFormat:@"有兩個引數: %@, %@ !!",array.firstObject,array.lastObject];
        [self showMsg:info];
    }
}

複製程式碼

網頁載入完成之後呼叫JS程式碼才會執行,因為這個時候html頁面已經注入到webView中並且可以響應到對應方法。OC呼叫JS程式碼

//不帶引數
- (IBAction)NOParameter:(id)sender {
    [self.webView evaluateJavaScript:@"alertMobile()" completionHandler:^(id _Nullable response, NSError * _Nullable error) {
        //JS 返回結果
        NSLog(@"%@ %@",response,error);
    }];
}
//一個引數
- (IBAction)oneParameter:(id)sender {
    /*
     *alertName(`奧特們打小怪獸`)
     *alertName JS方法名
     *奧特們打小怪獸 帶的引數
     */
    [self.webView evaluateJavaScript:@"alertName(`奧特們打小怪獸`)" completionHandler:nil];
}
//兩個引數
- (IBAction)twoParameter:(id)sender {
    /*
     *alertSendMsg(`我是引數1`,`我是引數2`)
     *alertSendMsg JS方法名
     *我是引數1 帶的引數
     *我是引數2
     */
    [self.webView evaluateJavaScript:@"alertSendMsg(`我是引數1`,`我是引數2`)" completionHandler:nil];
}

- (void)showMsg:(NSString *)msg {
    [[[UIAlertView alloc] initWithTitle:nil message:msg delegate:nil cancelButtonTitle:nil otherButtonTitles:@"OK", nil] show];
}
複製程式碼

解決POST請求傳參不管用問題

當我們用UIWebView POST請求傳參時一般是這樣寫的

// 建立WebView
    UIWebView *webView = [[UIWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
    // 設定訪問的URL
    NSURL *url = [NSURL URLWithString:@"http://www.example.com"];
    // 根據URL建立請求
    NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:url];
    // 設定請求方法為POST
    [request setHTTPMethod:@"POST"];
    // 設定請求引數
    [request setHTTPBody:[@"username=aaa&password=123" dataUsingEncoding:NSUTF8StringEncoding]];
    // WebView載入請求
    [webView loadRequest:request];
    // 將WebView新增到檢視
    [self.view addSubview:webView];
複製程式碼

但是當我們用WKWebView這樣載入的時候並沒有什麼卵用。

解決方法

參考文章:
http://stackoverflow.com/questions/26253133/cant-set-headers-on-my-wkwebview-post-request
http://www.jianshu.com/p/403853b63537

1、將一個包含JavaScript的POST請求的HTML程式碼放到工程目錄中
2、載入這個包含JavaScript的POST請求的程式碼到WKWebView
3、載入完成之後,用Native呼叫JavaScript的POST方法並傳入引數來完成請求

1、建立包含JavaScript的POST請求的HTML程式碼
<html>
 <head>
     <script>
         //呼叫格式: post(`URL`, {"key": "value"});
         function post(path, params) {
             var method = "post";
             var form = document.createElement("form");
             form.setAttribute("method", method);
             form.setAttribute("action", path);

             for(var key in params) {
                 if(params.hasOwnProperty(key)) {
                     var hiddenField = document.createElement("input");
                     hiddenField.setAttribute("type", "hidden");
                     hiddenField.setAttribute("name", key);
                     hiddenField.setAttribute("value", params[key]);

                     form.appendChild(hiddenField);
                 }
             }
             document.body.appendChild(form);
             form.submit();
         }
     </script>
 </head>
 <body>
 </body>
</html>
複製程式碼

將這段程式碼拷貝下來,然後貼上到文字編輯器中,名字可以隨意起,比方說儲存為:JSPOST.html,然後拷貝到工程目錄中,記得選擇對應的Target和勾選Copy items if needed(預設應該是勾選的)。這時候,就可以用這段JavaScript程式碼來傳送帶引數的POST請求了。

######2、將對應的JavaScript程式碼通過載入本地網頁的形式載入到WKWebView

 // JS傳送POST的Flag,為真的時候會呼叫JS的POST方法(僅當第一次的時候載入本地JS)
 self.needLoadJSPOST = YES;
 // 建立WKWebView
 self.webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];
 //設定代理
 self.webView.navigationDelegate = self;
 // 獲取JS所在的路徑
 NSString *path = [[NSBundle mainBundle] pathForResource:@"JSPOST" ofType:@"html"];
 // 獲得html內容
 NSString *html = [[NSString alloc] initWithContentsOfFile:path encoding:NSUTF8StringEncoding error:nil];
 // 載入js
 [self.webView loadHTMLString:html baseURL:[[NSBundle mainBundle] bundleURL]];
 // 將WKWebView新增到當前View
 [self.view addSubview:self.webView];
複製程式碼
3、Native呼叫JavaScript指令碼並傳入引數來完成POST請求

這裡需要用到WKWebView和JS的互動

 // 載入完成的代理方法
 - (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
     // 判斷是否需要載入(僅在第一次載入)
     if (self.needLoadJSPOST) {
         // 呼叫使用JS傳送POST請求的方法
         [self postRequestWithJS];
         // 將Flag置為NO(後面就不需要載入了)
         self.needLoadJSPOST = NO;
     }
 }

 // 呼叫JS傳送POST請求
 - (void)postRequestWithJS {
     // 傳送POST的引數
     NSString *postData = @""username":"aaa","password":"123"";
     // 請求的頁面地址
     NSString *urlStr = @"http://www.postexample.com";
     // 拼裝成呼叫JavaScript的字串
     NSString *jscript = [NSString stringWithFormat:@"post(`%@`, {%@});", urlStr, postData];

     // NSLog(@"Javascript: %@", jscript);
     // 呼叫JS程式碼
     [self.webView evaluateJavaScript:jscript completionHandler:^(id object, NSError * _Nullable error) {

     }];
 }
複製程式碼

github地址:https://github.com/jiangjunhui1993/KWWebView-JS-

相關文章