1. 程式人生 > >iOS WKWebView 遠端h5優先載入本地資源

iOS WKWebView 遠端h5優先載入本地資源

前言:UIWebView呼叫遠端h5頁面,優先載入本地圖片、js、css等資源,解決辦法就是對請求進行攔截。

服務端程式碼放在本文後面

客戶端需要對NSURLProtocol 的自定義類進行註冊,那麼所有的webview 對http請求都會被他攔截到;

首先自定義NSURLProtocol類

#import <Foundation/Foundation.h>
#import <CoreFoundation/CoreFoundation.h>
#import <MobileCoreServices/MobileCoreServices.h>

@interface NSURLProtocolCustom : NSURLProtocol

@end
#import "NSURLProtocolCustom.h"

static NSString* const FilteredKey = @"FilteredKey";

@implementation NSURLProtocolCustom

+ (BOOL)canInitWithRequest:(NSURLRequest *)request
{
    NSString *extension = request.URL.pathExtension;
    BOOL isSource = [@[@"png", @"jpeg", @"gif", @"jpg", @"js", @"css"] indexOfObjectPassingTest:^BOOL(id  _Nonnull obj, NSUInteger idx, BOOL * _Nonnull stop) {
        return [extension compare:obj options:NSCaseInsensitiveSearch] == NSOrderedSame;
    }] != NSNotFound;
    return [NSURLProtocol propertyForKey:FilteredKey inRequest:request] == nil && isSource;
}

+ (NSURLRequest *)canonicalRequestForRequest:(NSURLRequest *)request
{
    return request;
}

- (void)startLoading
{
    NSString *fileName = [super.request.URL.absoluteString componentsSeparatedByString:@"/"].lastObject;
    NSLog(@"fileName is %@",fileName);
    //這裡是獲取本地資源路徑 如:png,js等
    NSString *path = [[NSBundle mainBundle] pathForResource:fileName ofType:nil];
    if (!path) {
        [self sendResponseWithData:[NSData data] mimeType:nil];
        return;
    }
    
    //根據路徑獲取MIMEType
    CFStringRef pathExtension = (__bridge_retained CFStringRef)[path pathExtension];
    CFStringRef type = UTTypeCreatePreferredIdentifierForTag(kUTTagClassFilenameExtension, pathExtension, NULL);
    CFRelease(pathExtension);
    
    //The UTI can be converted to a mime type:
    NSString *mimeType = (__bridge_transfer NSString *)UTTypeCopyPreferredTagWithClass(type, kUTTagClassMIMEType);
    if (type != NULL)
        CFRelease(type);
    
    //載入本地資源
    NSData *data = [NSData dataWithContentsOfFile:path];
    [self sendResponseWithData:data mimeType:mimeType];
}

- (void)stopLoading
{
    NSLog(@"stopLoading, something went wrong!"); 
}

- (void)sendResponseWithData:(NSData *)data mimeType:(nullable NSString *)mimeType
{
    // 這裡需要用到MIMEType
    NSURLResponse *response = [[NSURLResponse alloc] initWithURL:super.request.URL
                                                        MIMEType:mimeType
                                           expectedContentLength:-1
                                                textEncodingName:nil];
    
    //硬編碼 開始嵌入本地資源到web中
    [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed];
    [[self client] URLProtocol:self didLoadData:data];
    [[self client] URLProtocolDidFinishLoading:self];
}

@end
其次實現對類的註冊
#import "CSWebView.h"
#import <WebKit/WebKit.h>
#import "NSURLProtocolCustom.h"

@interface CSWebView ()<WKNavigationDelegate,WKUIDelegate,WKScriptMessageHandler>

@property (nonatomic, strong) WKWebView *wkWebView;

@end

@implementation CSWebView

- (void)viewDidLoad {
    [super viewDidLoad];
    //1.設定背景顏色
    self.view.backgroundColor = [UIColor whiteColor];
    //2.註冊
    [NSURLProtocol registerClass:[NSURLProtocolCustom class]];
    //3.實現攔截功能,這個是核心
    Class cls = NSClassFromString(@"WKBrowsingContextController");
    SEL sel = NSSelectorFromString(@"registerSchemeForCustomProtocol:");
    if ([(id)cls respondsToSelector:sel]) {
#pragma clang diagnostic push
#pragma clang diagnostic ignored "-Warc-performSelector-leaks"
        [(id)cls performSelector:sel withObject:@"http"];
        [(id)cls performSelector:sel withObject:@"https"];
#pragma clang diagnostic pop
    }
    
    //4.新增WKWebView
    [self addWKWebView];
    
}

#pragma mark - WKWebView(IOS8以上使用)
#pragma mark 新增WKWebView
- (void)addWKWebView
{
    //1.建立配置項
    WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init];
    config.selectionGranularity = WKSelectionGranularityDynamic;
    
    //1.1 設定偏好
    config.preferences = [[WKPreferences alloc] init];
    config.preferences.minimumFontSize = 10;
    config.preferences.javaScriptEnabled = YES;
    //1.1.1 預設是不能通過JS自動開啟視窗的,必須通過使用者互動才能開啟
    config.preferences.javaScriptCanOpenWindowsAutomatically = NO;
    config.processPool = [[WKProcessPool alloc] init];
    
    //1.2 通過JS與webview內容互動配置
    config.userContentController = [[WKUserContentController alloc] init];
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
    
    //2.新增WKWebView
    WKWebView *wkWebView = [[WKWebView alloc] initWithFrame:CGRectMake(0, 0, [UIApplication sharedApplication].keyWindow.bounds.size.width, [UIApplication sharedApplication].keyWindow.bounds.size.height) configuration:config];
    wkWebView.autoresizingMask = UIViewAutoresizingFlexibleHeight | UIViewAutoresizingFlexibleWidth ;
    wkWebView.UIDelegate = self;
    wkWebView.navigationDelegate = self;
    
    _urlStr = [_urlStr stringByAddingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    NSURL *url = [NSURL URLWithString:_urlStr];
    [wkWebView loadRequest:[NSURLRequest requestWithURL:url]];
    
    //[wkWebView loadRequest: [[NSURLRequest alloc] initWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"app" ofType:@"html"]]]];
    
    [self.view addSubview:wkWebView];
    _wkWebView = wkWebView;
    
    //3.註冊js方法
    [config.userContentController addScriptMessageHandler:self name:@"webViewApp"];
}

- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //接受傳過來的訊息從而決定app呼叫的方法
    NSDictionary *dict = message.body;
    NSString *method = [dict objectForKey:@"method"];
    if ([method isEqualToString:@"hello"]) {
        [self hello:[dict objectForKey:@"param1"]];
    }else if ([method isEqualToString:@"Call JS"]){
        [self callJS];
    }else if ([method isEqualToString:@"Call JS Msg"]){
        [self callJSMsg:[dict objectForKey:@"param1"]];
    }
}

- (void)hello:(NSString *)param{
    NSLog(@"hello");
}

- (void)callJS{
    NSLog(@"callJS");
}

- (void)callJSMsg:(NSString *)msg{
    NSLog(@"callJSMsg");
}

//WKNavigationDelegate
// 頁面開始載入時呼叫
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {// 類似UIWebView的 -webViewDidStartLoad:
    NSLog(@"didStartProvisionalNavigation");
    [UIApplication sharedApplication].networkActivityIndicatorVisible = YES;
}
// 當內容開始返回時呼叫
- (void)webView:(WKWebView *)webView didCommitNavigation:(WKNavigation *)navigation {
    NSLog(@"didCommitNavigation");
}
// 頁面載入完成之後呼叫
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation { // 類似UIWebView 的 -webViewDidFinishLoad:
    NSLog(@"didFinishNavigation");
    //[self resetControl];
    if (webView.title.length > 0) {
        self.title = webView.title;
    }
    
}
// 頁面載入失敗時呼叫
- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(WKNavigation *)navigation withError:(NSError *)error {
    // 類似 UIWebView 的- webView:didFailLoadWithError:
    NSLog(@"didFailProvisionalNavigation");
}


// 在收到響應後,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler {
    decisionHandler(WKNavigationResponsePolicyAllow);
}

// 在傳送請求之前,決定是否跳轉
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction*)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    // 類似 UIWebView 的 -webView: shouldStartLoadWithRequest: navigationType:
    NSLog(@"decidePolicyForNavigationAction %@",navigationAction.request);
    //    NSString *url = [navigationAction.request.URL.absoluteString stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
    //    NSString *url = navigationAction.request.URL.absoluteString;
    decisionHandler(WKNavigationActionPolicyAllow);
    
}

// 接收到伺服器跳轉請求之後呼叫
- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(WKNavigation *)navigation{
    NSLog(@"didReceiveServerRedirectForProvisionalNavigation");
}


//- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition, NSURLCredential *))completionHandler {
//    completionHandler(NSURLSessionAuthChallengePerformDefaultHandling,internal);
//}


//WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration forNavigationAction:(WKNavigationAction*)navigationAction windowFeatures:(WKWindowFeatures *)windowFeatures {
    // 介面的作用是開啟新視窗委託
    //[self createNewWebViewWithURL:webView.URL.absoluteString config:Web];
    
    return _wkWebView;
}

- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{    // js 裡面的alert實現,如果不實現,網頁的alert函式無效
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message
                                                                             message:nil
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"確定"
                                                        style:UIAlertActionStyleCancel
                                                      handler:^(UIAlertAction *action) {
                                                          completionHandler();
                                                      }]];
    
    [self presentViewController:alertController animated:YES completion:^{
        
    }];
    
}

//  js 裡面的alert實現,如果不實現,網頁的alert函式無效
- (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString*)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL))completionHandler {
    
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:message
                                                                             message:nil
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    [alertController addAction:[UIAlertAction actionWithTitle:@"確定"
                                                        style:UIAlertActionStyleDefault
                                                      handler:^(UIAlertAction *action) {
                                                          completionHandler(YES);
                                                      }]];
    [alertController addAction:[UIAlertAction actionWithTitle:@"取消"
                                                        style:UIAlertActionStyleCancel
                                                      handler:^(UIAlertAction *action){
                                                          completionHandler(NO);
                                                      }]];
    
    [self presentViewController:alertController animated:YES completion:^{}];
    
}

- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void(^)(NSString *))completionHandler {
    
    completionHandler(@"Client Not handler");
    
}

- (void)showAlert:(NSString *)content Title:(NSString *)title{
    UIAlertController *alertController = [UIAlertController alertControllerWithTitle:title
                                                                             message:content
                                                                      preferredStyle:UIAlertControllerStyleAlert];
    
    [alertController addAction:[UIAlertAction actionWithTitle:@"確定"
                                                        style:UIAlertActionStyleCancel
                                                      handler:^(UIAlertAction *action) {
                                                          [self.navigationController popToRootViewControllerAnimated:YES];
                                                      }]];
    [self presentViewController:alertController animated:YES completion:^{
        
    }];
}

@end

服務端程式碼
<!DOCTYPE html>
<html>
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <title>測試iOS與JS之間的互調</title>
    <style type="text/css">
        {
            font-size: 40px;
        }
    </style>
</head>
<body>
<div style="rargin-top: 100px;">
    <h1>Test how to use Objective-C call js</h1>
    <input type="button" value="Call iOS" onclick="calliOS('call iOS')">
    <input type="button" value="Call JS Alert" onclick="jsFunc()">
</div>
<div>
    <input type="button" value="iOS Call With No JSON" onclick="callJS()">
    <input type="button" value="iOS Call With JSON" onclick="callJSMsg('iOS Call JS')">
</div>
<div>
    <span id="jsParatFuncSpan" style="color:red; font-size:50px;"></span>
</div>
</body>
<script type="text/JavaScript" src="appJs.js"></script>
</html>
本地js檔案
function calliOS(Msg) {
    var message = {
        'method' : 'hello',
        'param1' : Msg,
    };
    window.webkit.messageHandlers.webViewApp.postMessage(message);
}

function callJS() {
    var message = {
        'method' : 'Call JS',
    };
    window.webkit.messageHandlers.webViewApp.postMessage(message);
}

function callJSMsg(Msg) {
    var message = {
        'method' : 'Call JS Msg',
        'param1' : Msg,
    };
    window.webkit.messageHandlers.webViewApp.postMessage(message);
}

function jsFunc() {
    alert('Hello World');
}