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

一、Native開發(fā)中為什么需要H5容器

Native開發(fā)原生應用是手機操作系統(tǒng)廠商(目前主要是蘋果的iOS和google的Android)對外界提供的標準化的開發(fā)模式,他們對于native開發(fā)提供了一套標準化實現(xiàn)和優(yōu)化方案。但是他們存在一些硬傷,比如App的發(fā)版周期偏長、有時無法跟上產(chǎn)品的更新節(jié)奏;靈活性差,如果有較大的方案變更,需要發(fā)版才能解決;如果存在bug,在當前版本修復的難度比較大(iOS的JSPatch方案和Android的Dex修復方案);需要根據(jù)不同的平臺寫不同的代碼,iOS主要為object_c和swift,android為Java。

而作為H5為主要開發(fā)模式的Web App的靈活性就比較強,他利用操作系統(tǒng)中的h5容器作為一個承載,對外提供一個url鏈接,而該url鏈接對應的內(nèi)容可以實時在服務(wù)端進行修改,靈活行很強,避免了Native發(fā)版周期帶來的時間成本。但是h5雖然靈活,但是他也有自己的硬傷。每次都需要下載完整的UI數(shù)據(jù)(html,css,js),弱網(wǎng)用戶體驗較差,流量消耗較大;無法調(diào)用系統(tǒng)文件系統(tǒng),硬件資源等等;

Native App和Web App都有他們的優(yōu)勢和劣勢。我們也不能一棍子拍死說誰好誰劣。通常的經(jīng)驗是:對于一些比較穩(wěn)當?shù)臉I(yè)務(wù),對用戶體驗要求較高的,我們可以選擇Native開發(fā)。而對于一些業(yè)務(wù)變更比較快、處在不斷試水的過程,而且不涉及調(diào)用文件系統(tǒng)和硬件調(diào)用的業(yè)務(wù)我們可以選擇h5開發(fā)。所以說,在一款app中我們需要同時支持Native代碼和h5代碼。這也是我們標題所說的Native開發(fā)中需要H5容器的必要性。

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

二、UIWebView的基本用法

2.1、加載網(wǎng)頁

    UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    webView.delegate = self;
    [self.view addSubview:webView];
    //網(wǎng)絡(luò)地址
    NSURL *url = [[NSURL alloc] initWithString:@"http://www.taobao.com"];
    NSURLRequest *request = [NSURLRequest requestWithURL:url];
    [webView loadRequest:request];

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

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

2.3、Native調(diào)用JS中的方法

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

<script type="text/javascript">
function hello(){
    alert("你好!");
}

function helloWithName(name){
    alert(name + ",你好!");
}
</script>

我們可以調(diào)用<code>- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;</code>函數(shù)進行js調(diào)用。

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

js代碼不一定要在js文件中預留,也可以在代碼中通過字符串的形式進行調(diào)用,比如下面:

    //自定義js函數(shù)
    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中調(diào)用Naitve的方法

具體讓js通知native進行方法調(diào)用,我們可以讓js產(chǎn)生一個特殊的請求??梢宰孨ative代碼可以攔截到,而且不然用戶察覺。業(yè)界一般的實現(xiàn)方案是在網(wǎng)頁中加載一個隱藏的iframe來實現(xiàn)該功能。通過將iframe的src指定為一個特殊的URL,實現(xiàn)在<code>- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;</code>方案中進行攔截處理。對應的js調(diào)用代碼如下:

    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);
        // 發(fā)起請求后這個iFrame就沒用了,所以把它從dom上移除掉
        iFrame.parentNode.removeChild(iFrame);
        iFrame = null;
    }

比如我們在js代碼中,調(diào)用一下兩個js方法:

    function iOS_alert() {//調(diào)用自定義對話框
        loadURL("alert://abc");
    }
    function call() {//  js中進行撥打電話處理
        loadURL("tel://17715022071");
    }

當你觸發(fā)以上方法的時候,就會進入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];
        return NO;
    }else if([[url scheme] isEqualToString:@"tel"]){//攔截撥打電話請求
        BOOL result = [[UIApplication sharedApplication] openURL:url];
        if (!result) {
            NSLog(@"您的設(shè)備不支持打電話");
        } else {
            NSLog(@"電話打了");
        }
        return NO;
    }
    
    return YES;
}

這樣我們就可以讓js進行native的調(diào)用。

三、WKWebView的基本用法

3.1、加載網(wǎng)頁

    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、幾個常用的代理方法

/**
 *  根據(jù)webView、navigationAction相關(guān)信息決定這次跳轉(zhuǎn)是否可以繼續(xù)進行,這些信息包含HTTP發(fā)送請求,如頭部包含User-Agent,Accept,refer
 *  在發(fā)送請求之前,決定是否跳轉(zhuǎn)的代理
 *  @param webView
 *  @param navigationAction
 *  @param decisionHandler
 */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
    decisionHandler(WKNavigationActionPolicyAllow);
}

/**
 *  這個代理方法表示當客戶端收到服務(wù)器的響應頭,根據(jù)response相關(guān)信息,可以決定這次跳轉(zhuǎn)是否可以繼續(xù)進行。
 *  在收到響應后,決定是否跳轉(zhuǎn)的代理
 *  @param webView
 *  @param navigationResponse
 *  @param decisionHandler
 */
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler{
    decisionHandler(WKNavigationResponsePolicyAllow);
}

/**
 *  準備加載頁面。等同于UIWebViewDelegate: - webView:shouldStartLoadWithRequest:navigationType
 *
 *  @param webView
 *  @param navigation
 */
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
}

/**
 *  這個代理是服務(wù)器redirect時調(diào)用
 *  接收到服務(wù)器跳轉(zhuǎn)請求的代理
 *  @param webView
 *  @param navigation
 */
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation{
    
}

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

/**
 *  內(nèi)容開始加載. 等同于UIWebViewDelegate: - webViewDidStartLoad:
 *
 *  @param webView
 *  @param navigation
 */
- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation{
    
}

/**
 *  頁面加載完成。 等同于UIWebViewDelegate: - webViewDidFinishLoad:
 *
 *  @param webView
 *  @param navigation
 */
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation{
    
}

/**
 *  頁面加載失敗。 等同于UIWebViewDelegate: - webView:didFailLoadWithError:
 *
 *  @param webView
 *  @param navigation
 *  @param error      
 */
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error{
    
}

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

/*
 我們看看WKUIDelegate的幾個代理方法,雖然不是必須實現(xiàn)的,但是如果我們的頁面中有調(diào)用了js的alert、confirm、prompt方法,我們應該實現(xiàn)下面這幾個代理方法,然后在原來這里調(diào)用native的彈出窗,因為使用WKWebView后,HTML中的alert、confirm、prompt方法調(diào)用是不會再彈出窗口了,只是轉(zhuǎn)化成ios的native回調(diào)代理方法
 */
#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顆粒度更細的方法。讓開發(fā)者可以進行更加細致的配置和處理。

3.3 、Native調(diào)用JS中的方法

WKWebView提供的調(diào)用js代碼的函數(shù)是:

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

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

<script type="text/javascript">
function hello(){
    alert("你好!");
}

function helloWithName(name){
    alert(name + ",你好!");
}
</script>

我們可以調(diào)用如下代碼進行js的調(diào)用:

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

同UIWebView一樣,我們也可以通過字符串的形式進行js調(diào)用。

    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中調(diào)用Naitve的方法

除了和UIWebView加載一個隱藏的ifame之外,WKWebView自身還提供了一套js調(diào)用native的規(guī)范。

我們可以在初始化WKWebView的時候,給他設(shè)置一個config參數(shù)。

//高端配置
    //創(chuàng)建配置
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    //創(chuàng)建UserContentController(提供javaScript向webView發(fā)送消息的方法)
    WKUserContentController *userContent = [[WKUserContentController alloc] init];
    //添加消息處理,注意:self指代的是需要遵守WKScriptMessageHandler協(xié)議,結(jié)束時需要移除
    [userContent addScriptMessageHandler:self name:@"NativeMethod"];
    //將UserContentController設(shè)置到配置文件中
    config.userContentController = userContent;
    //高端的自定義配置創(chuàng)建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代碼調(diào)用native。

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

<script type="text/javascript">
    function invokeNativeMethod(){
        window.webkit.messageHandlers.NativeMethod.postMessage("我要調(diào)用native的方法");
    }
</script>

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

#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //這里就是使用高端配置,js調(diào)用native的處理地方。我們可以根據(jù)name和body,進行橋協(xié)議的處理。
    NSString *messageName = message.name;
    if ([@"NativeMethod" isEqualToString:messageName]) {
        id messageBody = message.body;
        NSLog(@"%@",messageBody);
    }
}

四、UIWebView和WKWebView的比較和選擇

WKWebView是蘋果在WWDC2014發(fā)布會中發(fā)布IOS8的時候公布WebKit時候使用的新型的H5容器。它與UIWebView相比較,擁有更快的加載速度和性能,更低的內(nèi)存占用。將UIWebViewDelegate和UIWebView重構(gòu)成了14個類,3個協(xié)議,可以讓開發(fā)者進行更加細致的配置。

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

五、聯(lián)系方式

新浪微博
github
簡書首頁

歡迎加好友、一起交流。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容