天天看點

iOS H5容器的一些探究:UIWebView和WKWebView的比較和選擇

一、Native開發中為什麼需要H5容器

Native開發原生應用是手機作業系統廠商(目前主要是蘋果的iOS和google的Android)對外界提供的标準化的開發模式,他們對于native開發提供了一套标準化實作和優化方案。但是他們存在一些硬傷,比如App的發版周期偏長、有時無法跟上産品的更新節奏;靈活性差,如果有較大的方案變更,需要發版才能解決;如果存在bug,在目前版本修複的難度比較大(iOS的JSPatch方案和Android的Dex修複方案);需要根據不同的平台寫不同的代碼,iOS主要為object_c和swift,android為Java。

而作為H5為主要開發模式的Web App的靈活性就比較強,他利用作業系統中的h5容器作為一個承載,對外提供一個url連結,而該url連結對應的内容可以實時在服務端進行修改,靈活行很強,避免了Native發版周期帶來的時間成本。但是h5雖然靈活,但是他也有自己的硬傷。每次都需要下載下傳完整的UI資料(html,css,js),弱網使用者體驗較差,流量消耗較大;無法調用系統檔案系統,硬體資源等等;

Native App和Web App都有他們的優勢和劣勢。我們也不能一棍子拍死說誰好誰劣。通常的經驗是:對于一些比較穩當的業務,對使用者體驗要求較高的,我們可以選擇Native開發。而對于一些業務變更比較快、處在不斷試水的過程,而且不涉及調用檔案系統和硬體調用的業務我們可以選擇h5開發。是以說,在一款app中我們需要同時支援Native代碼和h5代碼。這也是我們标題所說的Native開發中需要H5容器的必要性。

iOS存在的h5容器主要包括UIWebView和WKWebView,下面我們就分别來說說他們的用法和優劣。

二、UIWebView的基本用法

2.1、加載網頁

UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];

webView.delegate = self;

[self.view addSubview:webView];//網絡位址

NSURL *url = [[NSURL alloc] initWithString:@"http://www.taobao.com"];    NSURLRequest *request = [NSURLRequest requestWithURL:url];

[webView loadRequest:request];

2.2、UIWebViewDelegate幾個常用的代理方法

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;//進行加載前的預判斷,如果傳回YES,則會進入後續流程(StartLoad,FinishLoad)。如果傳回NO,這不會進入後續流程。- (void)webViewDidStartLoad:(UIWebView *)webView;//開始加載網頁- (void)webViewDidFinishLoad:(UIWebView *)webView;//加載完成- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;//加載失敗

2.3、Native調用JS中的方法

比如我們在加載的HTML檔案中有如下js代碼:

function hello(){

alert("你好!");

}function helloWithName(name){

alert(name +",你好!");

}

我們可以調用- (nullable NSString)stringByEvaluatingJavaScriptFromString:(NSString)script;函數進行js調用。

[webView stringByEvaluatingJavaScriptFromString:@"hello()"];[webView stringByEvaluatingJavaScriptFromString:@"helloWithName('jack')"];

js代碼不一定要在js檔案中預留,也可以在代碼中通過字元串的形式進行調用,比如下面:

//自定義js函數

NSString *jsString = @"function sayHello(){ \                                alert('jack11')   \                            }                   \                           sayHello()";    [_webView stringByEvaluatingJavaScriptFromString:jsString];

NSString *jsString = @" var p = document.createElement('p'); \                            p.innerText = 'New Line';            \                            document.body.appendChild(p);        \    ";    [_webView stringByEvaluatingJavaScriptFromString:jsString];

2.4、JS中調用Naitve的方法

具體讓js通知native進行方法調用,我們可以讓js産生一個特殊的請求。可以讓Native代碼可以攔截到,而且不然使用者察覺。業界一般的實作方案是在網頁中加載一個隐藏的iframe來實作該功能。通過将iframe的src指定為一個特殊的URL,實作在- (BOOL)webView:(UIWebView)webView shouldStartLoadWithRequest:(NSURLRequest)request navigationType:(UIWebViewNavigationType)navigationType;方案中進行攔截處理。對應的js調用代碼如下:

function loadURL(url) {        var iFrame;

iFrame = document.createElement("iframe");

iFrame.setAttribute("src", url);

iFrame.setAttribute("style","display:none;");

iFrame.setAttribute("height","0px");

iFrame.setAttribute("width","0px");

iFrame.setAttribute("frameborder","0");        document.body.appendChild(iFrame);// 發起請求後這個iFrame就沒用了,是以把它從dom上移除掉

iFrame.parentNode.removeChild(iFrame);

iFrame = null;

}

比如我們在js代碼中,調用一下兩個js方法:

function iOS_alert() {//調用自定義對話框

loadURL("alert://abc");

}    function call() {//  js中進行撥打電話處理

loadURL("tel://17715022071");

}

當你觸發以上方法的時候,就會進入webview的代理方法中進行攔截。

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{    NSURL * url = [request URL];if([[url scheme] isEqualToString:@"alert"]) {//攔截請求,彈出自定義對話框

UIAlertView * alertView = [[UIAlertView alloc] initWithTitle:@"test"message:[url host] delegate:nil cancelButtonTitle:@"OK"otherButtonTitles:nil];

[alertView show];returnNO;

}elseif([[url scheme] isEqualToString:@"tel"]){//攔截撥打電話請求

BOOLresult = [[UIApplication sharedApplication] openURL:url];if(!result) {            NSLog(@"您的裝置不支援打電話");

}else{            NSLog(@"電話打了");

}returnNO;

}returnYES;

}

這樣我們就可以讓js進行native的調用。

三、WKWebView的基本用法

3.1、加載網頁

WKWebView *webView = [[WKWebView alloc] initWithFrame:[UIScreen mainScreen].bounds];    NSURL *url = [NSURL URLWithString:@"http://www.taobao.com"];    NSURLRequest *request = [NSURLRequest requestWithURL:url];

[webView loadRequest:request];

[self.view addSubview:webView];

3.2、幾個常用的代理方法

*  根據webView、navigationAction相關資訊決定這次跳轉是否可以繼續進行,這些資訊包含HTTP發送請求,如頭部包含User-Agent,Accept,refer

*  在發送請求之前,決定是否跳轉的代理

*  @param webView

*  @param navigationAction

*  @param decisionHandler

*/- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void(^)(WKNavigationActionPolicy))decisionHandler{

decisionHandler(WKNavigationActionPolicyAllow);

}- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void(^)(WKNavigationResponsePolicy))decisionHandler{

decisionHandler(WKNavigationResponsePolicyAllow);

}- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{

}- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation{

}

- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error{

}- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation{

}- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{

}- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error{

}

- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0){

}#pragma mark - WKUIDelegate- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(void))completionHandler{    UIAlertController *alertView = [UIAlertController alertControllerWithTitle:@"h5Container"message:message preferredStyle:UIAlertControllerStyleAlert];//    [alertView addTextFieldWithConfigurationHandler:^(UITextField * _Nonnull textField) {//        textField.textColor = [UIColor redColor];//    }];

[alertView addAction:[UIAlertAction actionWithTitle:@"我很确定"style:UIAlertActionStyleDefault handler:^(UIAlertAction * _Nonnull action) {

completionHandler();

}]];

[self presentViewController:alertView animated:YES completion:nil];

}

顯然WKWebView的代理方法提供了比UIWebView顆粒度更細的方法。讓開發者可以進行更加細緻的配置和處理。

3.3 、Native調用JS中的方法

WKWebView提供的調用js代碼的函數是:

1

- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void(^ __nullable)(__nullable id, NSError * __nullable error))completionHandler;

比如我們在加載的HTML檔案中有如下js代碼:

function hello(){

alert("你好!");

}function helloWithName(name){

alert(name +",你好!");

}

我們可以調用如下代碼進行js的調用:

[_wkView evaluateJavaScript:@"hello()"completionHandler:^(id item, NSError * error) {

}];

[_wkView evaluateJavaScript:@"helloWithName('jack')"completionHandler:^(id item, NSError *error) {

}];

同UIWebView一樣,我們也可以通過字元串的形式進行js調用。

NSString *jsString = @"function sayHello(){ \

alert('jack11')   \

}                   \

sayHello()";

[_wkView evaluateJavaScript:jsString completionHandler:^(id item, NSError *error) {

}];

jsString = @" var p = document.createElement('p'); \

p.innerText ='New Line';            \

document.body.appendChild(p);        \

";

[_wkView evaluateJavaScript:jsString completionHandler:^(id item, NSError *error) {

}];

3.4、JS中調用Naitve的方法

除了和UIWebView加載一個隐藏的ifame之外,WKWebView自身還提供了一套js調用native的規範。

我們可以在初始化WKWebView的時候,給他設定一個config參數。

//高端配置

//建立配置

WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];//建立UserContentController(提供javaScript向webView發送消息的方法)

WKUserContentController *userContent = [[WKUserContentController alloc] init];//添加消息處理,注意:self指代的是需要遵守WKScriptMessageHandler協定,結束時需要移除

[userContent addScriptMessageHandler:self name:@"NativeMethod"];//将UserContentController設定到配置檔案中

config.userContentController = userContent;//高端的自定義配置建立WKWebView

_wkView = [[YXWKView alloc] initWithFrame:self.view.bounds configuration:config];    NSURL *url = [NSURL URLWithString:@"http://localhost:8080/myDiary/index.html"];    NSURLRequest *request = [NSURLRequest requestWithURL:url];

[_wkView loadRequest:request];

_wkView.UIDelegate = self;

_wkView.navigationDelegate = self;

[self.view addSubview:_wkView];

我們在js可以通過NativeMethod這個Handler讓js代碼調用native。

比如在js代碼中,我新增了一個方法

    function invokeNativeMethod(){        window.webkit.messageHandlers.NativeMethod.postMessage("我要調用native的方法");

}

觸發以上方法的時候,會在native以下方法中進行攔截處理。

1#pragma mark - WKScriptMessageHandler- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{    //這裡就是使用高端配置,js調用native的處理地方。我們可以根據name和body,進行橋協定的處理。

NSString *messageName = message.name;if([@"NativeMethod"isEqualToString:messageName]) {        id messageBody = message.body;        NSLog(@"%@",messageBody);

}

}

四、UIWebView和WKWebView的比較和選擇

WKWebView是蘋果在WWDC2014釋出會中釋出IOS8的時候公布WebKit時候使用的新型的H5容器。它與UIWebView相比較,擁有更快的加載速度和性能,更低的記憶體占用。将UIWebViewDelegate和UIWebView重構成了14個類,3個協定,可以讓開發者進行更加細緻的配置。

但是他有一個最緻命的缺陷,就是WKWebView的請求不能被NSURLProtocol截獲。而我們團隊開發的app中對于H5容器最佳的優化點主要就在于使用NSURLProtocol技術對于H5進行離線包的處理和H5的圖檔和Native的圖檔公用一套緩存的技術。因為該問題的存在,目前我們團隊還沒有使用WKWebView代替UIWebVIew。