iOS高階-WebView & JavaScript互動(附DEMO)

weixin_34232744發表於2019-01-20

文中程式碼與html地址在 LJMElevateSelf in GitHub

WebView與JavaScript的互動方式

0.iOS7之前,原生與JavaScript的互動
1.JavaScriptCore(適用於UIWebView,iOS 7+)
2.WKScriptMessageHandler(適用於WKWebView,iOS 8+)
3.攔截協議(適用於UIWebView和WKWebView)
4.WebViewJavascriptBridge(適用於UIWebView和WKWebView的第三方框架,是基於攔截協議進行的封裝)

0. iOS7之前,原生與JavaScript的互動

原生 -> JavaScript

//UIWebView的方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

stringByEvaluatingJavaScriptFromString:只能在主執行緒執行;
通過此方法可以簡單呼叫系統提供的Javascript方法。例如:

// 獲取當前頁面的title
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];
// 獲取當前頁面的url
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];

JavaScript -> 原生

遵守UIWebViewDelegate並實現代理方法:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 
navigationType:(UIWebViewNavigationType)navigationType;

該方法可以監聽到UIWebView中發出的URL請求,通過與H5協商一個URL通訊協議,來攔截指定的URL,做相應的操作,並阻止此連結的跳轉。舉個例子:
html程式碼:

<!DOCTYPE html>
<html><head>
    <meta charset="UTF-8">
</head><body>
    <div style="margin-top: 10px">
        <input type="button" value="callPhone" onclick="callPhone()">
    </div>
</body><script>
    // 宣告一個名為callPhone的js函式,其會發出一個連結為nativejs://callPhone的請求
    function callPhone() {
        window.location.href = 'nativejs://callPhone';
    }
</script></html>

objc程式碼:

/**
 *  在一個網頁開始載入一個frame前被呼叫
 */
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request 
 navigationType:(UIWebViewNavigationType)navigationType {
    NSString *urlString = request.URL.absoluteString;
    NSRange range = [urlString rangeOfString:@"nativejs://"];
    if (range.location != NSNotFound) { // 攔截URL協議頭是nativejs的連結
        NSLog(@"執行原生呼叫的方法");
        return NO;// 阻止此連結的跳轉
    }
    return YES;
}

由於絕大多數APP已經支援iOS 7+,所以這裡只做簡單介紹。

1. JavaScriptCore

JavaScriptCore是iOS 7之後蘋果推出的框架可以讓互動變的更加方便。舉個例子:
JavaScriptCore.html內容:

<!DOCTYPE html>
<html><head>
    <meta charset="UTF-8">
</head><body>
    <div style="margin-top: 100px">
        <h1>JavaScriptCore的簡單介紹與使用</h1>
        <input type="button" value="呼叫相機" onclick="LJMcarryu.callCamera()">
    </div>
    <div>
        <input type="button" value="分享" onclick="callShare()">
    </div>
    <script>
        var callShare = function() {
        var shareInfo = JSON.stringify({"title": "標題",
                                        "description": "內容",
                                        "shareUrl": "https://www.jianshu.com/p/afbd98793c18",
                                        "shareImage":"http://a2.att.hudong.com/46/18/01300000309266122822186737333.jpg"
                                       });
        LJMcarryu.share(shareInfo);
        }
    
        var picCallback = function(photos) {
            alert(photos);
        }
        
        var shareCallback = function(){
            alert('分享成功');
        }
    </script>
</body></html>

上述程式碼比較簡單,定義兩個按鈕 “呼叫相機”、“分享” 其中“分享”傳有引數shareInfo;並在下方定義了兩個原生方法呼叫後的回撥方法picCallbackshareCallback

客戶端這邊根據web前端定義的方法名去完成互動,JavaScriptCore中web頁面呼叫原生應用的方法可以用DelegateBlock兩種方法,示例以Delegate來講。
Objc程式碼:

#import "BlocksKitViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>

@protocol JSOBJCDelegate <JSExport>

- (void)callCamera;
- (void)share:(NSString *)shareString;

@end

@interface BlocksKitViewController () <UIWebViewDelegate, JSOBJCDelegate>

@property (nonatomic, strong) JSContext *jsContext;

@end

@implementation BlocksKitViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
    webView.delegate = self;
    [self.view addSubview:webView];
    
    NSURL *url = [[NSBundle mainBundle] URLForResource:@"TexstJavaScriptCore" withExtension:@"html"];
    [webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];
    
}

#pragma mark - UIWebViewDelegate

- (void)webViewDidFinishLoad:(UIWebView *)webView {
    NSLog(@"網頁載入完成");
    self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
    self.jsContext[@"LJMcarryu"] = self;
    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
        context.exception = exceptionValue;
        NSLog(@"異常資訊:%@", exceptionValue);
    };
}

#pragma mark - JSObjcDelegate

- (void)callCamera {
    NSLog(@"呼叫相機");
    // 獲取到照片之後在回撥js的方法picCallback把圖片傳出去
    JSValue *picCallback = self.jsContext[@"picCallback"];
    [picCallback callWithArguments:@[@"這是圖片"]];
}

- (void)share:(NSString *)shareString {
    NSLog(@"分享內容:%@", shareString);
    // 分享成功回撥js的方法shareCallback
    JSValue *shareCallback = self.jsContext[@"shareCallback"];
    [shareCallback callWithArguments:nil];
}

在介紹程式碼內容前,先來看看JavaScriptCore的幾種類與協議:

JSExport:JavaScriptCore裡的協議,如果採用協議的方法進行互動,自定義的協議就必須遵守此協議;
JSContext:給JavaScript提供執行的上下文環境;
JSValue:JavaScript和Objective-C資料和方法的橋樑;
JSManagedValue:管理資料和方法的類;
JSVirtualMachine:處理執行緒相關

上述程式碼中:
自定義的JSOBJCDelegate必須遵守JSExport協議。
webView載入完成後獲取JavaScript執行的上下文環境,然後再注入物件名為LJMcarryu的橋樑,物件為selfself遵守此自定義協議並實現協議中對應的方法。
在JavaScript呼叫完本地方法做完相對應的事情之後,又回撥了JavaScript中對應的方法,從而實現了web頁面和本地應用之間的通訊。

2. WKScriptMessageHandler

這裡糾結是直接寫互動部分,還是比較系統的介紹WKWebView,回頭補上

3. 攔截協議

攔截協議使用時不需引入框架,只用於一些簡單的情況,因為其無法回撥JavaScript的方法
InterceptUrl.html程式碼:

<!DOCTYPE html>
<html><head>
    <meta charset="UTF-8">
</head><body>
    <div>
        <input type="button" value="呼叫相機" onclick="callCamera()">
    </div>
<script>
    function callCamera() {
        //改變主視窗指向併發出請求
        window.location.href = 'LJMcarryu://callCamera';
    }
</script>
</body></html>

非常簡單的頁面,只有一個按鈕來呼叫相機。

客戶端要攔截LJMcarryu://callCamera請求,根據請求內容來完成JavaScript想做的事情。
Objc程式碼:

- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSString *url = request.URL.absoluteString;
    if ([url rangeOfString:@"LJMcarryu://"].location != NSNotFound) { 
        // 攔截LJMcarryu://,阻止此連結的跳轉,返回NO
        NSLog(@"呼叫相機成功");
        return NO;
    }
    return YES;
}

4. WebViewJavascriptBridge

WebViewJavascriptBridge是基於攔截協議的封裝,讓其能夠較好的用於 WKWebView & UIWebView 中 OC 和 JS 的互動。也是舉個例子:

html檔案就借鑑WebViewJavascriptBridge自身demo裡的ExampleApp.html,做了簡單的修改:

<!doctype html>
<html><head>
    <meta charset="UTF-8" name="viewport" content="user-scalable=no, width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <style type='text/css'>
        html { font-family:Helvetica; color:#222; }
        h1 { color:steelblue; font-size:24px; margin-top:24px; }
        button { margin:0 3px 10px; font-size:12px; }
        .logLine { border-bottom:1px solid #ccc; padding:4px 2px; font-family:courier; font-size:11px; }
    </style>
</head><body>
    <h1>WebViewJavascriptBridge Demo</h1>
    <script>
    window.onerror = function(err) {
        log('window.onerror: ' + err)
    }

    function setupWebViewJavascriptBridge(callback) {
        if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
        if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
        window.WVJBCallbacks = [callback];
        var WVJBIframe = document.createElement('iframe');
        WVJBIframe.style.display = 'none';
        WVJBIframe.src = 'https://__bridge_loaded__';
        document.documentElement.appendChild(WVJBIframe);
        setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
    }
    //這一段程式碼是註冊OC將要呼叫的JS方法
    setupWebViewJavascriptBridge(function(bridge) {
        var uniqueId = 1
        function log(message, data) {
            var log = document.getElementById('log')
            var el = document.createElement('div')
            el.className = 'logLine'
            el.innerHTML = uniqueId++ + '. ' + message + ':<br/>' + JSON.stringify(data)
            if (log.children.length) { log.insertBefore(el, log.children[0]) }
            else { log.appendChild(el) }
        }
                //testJavascriptHandler01 是OC呼叫JS方法的函式名
                //data OC傳遞過來的資料
                //responseCallback 向OC傳遞的資料
        bridge.registerHandler('testJavascriptHandler01', function(data, responseCallback) {
            log('原生呼叫JS處理', data)
            var responseData = { 'JS說':'OK,I‘m ready!' }
            log('JS回應道', responseData)
            responseCallback(responseData)
        })
        //testJavascriptHandler02 也是OC呼叫JS方法的函式名
        //data OC傳遞過來的資料
        //responseCallback 向OC傳遞的資料
        bridge.registerHandler('testJavascriptHandler02', function(data, responseCallback) {
            log('原生呼叫JS處理', data)
            var responseData = { 'JS說':'OK,I see you!' }
            log('JS回應道', responseData)
            responseCallback(responseData)
        })

        document.body.appendChild(document.createElement('br'))

        var callbackButton = document.getElementById('buttons').appendChild(document.createElement('button'))
        callbackButton.innerHTML = '啟用原生回撥'
        callbackButton.onclick = function(e) {
            e.preventDefault()
            log('JS喚醒處理原生回撥','原生應我一聲')
            bridge.callHandler('testObjcCallback', {'foo': 'bar'}, function(response) {
                log('JS獲得回應', response)
            })
        }
    })
    </script>
    <div id='buttons'></div> <div id='log'></div>
</body></html>

上述程式碼改成了中文輸出,並加了一個橋樑註冊,完整程式碼 LJMElevateSelf in GitHub

Objc程式碼:

#import "WebViewJavascriptBridgeViewController.h"
#import "WebViewJavascriptBridge.h"

@interface WebViewJavascriptBridgeViewController ()

@property WebViewJavascriptBridge *bridge;

@end

@implementation WebViewJavascriptBridgeViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    
    if (_bridge) return;
    
    WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds];
    webView.navigationDelegate = self;
    [self.view addSubview:webView];
    //UIWebView
//    UIWebView *webView = [[UIWebView alloc] initWithFrame:self.view.bounds];
//    [self.view addSubview:webView];
    
    [WebViewJavascriptBridge enableLogging];
    _bridge = [WebViewJavascriptBridge bridgeForWebView:webView];
    [_bridge setWebViewDelegate:self];
    /*
     data               是JS傳遞過來的資料
     responseCallback   往JS傳遞的資料
     */
    [_bridge registerHandler:@"testObjcCallback" handler:^(id data, WVJBResponseCallback responseCallback) {
        NSLog(@"原生回撥呼叫內容: %@", data);
        responseCallback(@"來自原生回撥的回應");
    }];
    
    [_bridge callHandler:@"testJavascriptHandler01" data:@{ @"提醒訊息":@"JS做好準備" }];
    
    [self renderButtons:webView];
    [self loadExamplePage:webView];
}

- (void)renderButtons:(WKWebView*)webView {
    UIFont* font = [UIFont fontWithName:@"HelveticaNeue" size:12.0];
    
    UIButton *callbackButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [callbackButton setTitle:@"呼叫處理" forState:UIControlStateNormal];
    [callbackButton addTarget:self action:@selector(callHandler:) forControlEvents:UIControlEventTouchUpInside];
    [self.view insertSubview:callbackButton aboveSubview:webView];
    callbackButton.frame = CGRectMake(10, 400, 100, 35);
    callbackButton.titleLabel.font = font;
    
    UIButton* reloadButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
    [reloadButton setTitle:@"重載入檢視" forState:UIControlStateNormal];
    [reloadButton addTarget:webView action:@selector(reload) forControlEvents:UIControlEventTouchUpInside];
    [self.view insertSubview:reloadButton aboveSubview:webView];
    reloadButton.frame = CGRectMake(110, 400, 100, 35);
    reloadButton.titleLabel.font = font;
    
    //UIWebView
//    UIButton *safetyTimeoutButton = [UIButton buttonWithType:UIButtonTypeRoundedRect];
//    [safetyTimeoutButton setTitle:@"Disable safety timeout" forState:UIControlStateNormal];
//    [safetyTimeoutButton addTarget:self action:@selector(disableSafetyTimeout) forControlEvents:UIControlEventTouchUpInside];
//    [self.view insertSubview:safetyTimeoutButton aboveSubview:webView];
//    safetyTimeoutButton.frame = CGRectMake(190, 400, 120, 35);
//    safetyTimeoutButton.titleLabel.font = font;
}
//UIWebView
//- (void)disableSafetyTimeout {
//    [self.bridge disableJavscriptAlertBoxSafetyTimeout];
//}

- (void)callHandler:(id)sender {
    id data = @{ @"來自原生的問候": @"嗨,我在這 JS!" };
    [_bridge callHandler:@"testJavascriptHandler02" data:data responseCallback:^(id response) {
        NSLog(@"JS處理回應: %@", response);
    }];
}

- (void)loadExamplePage:(WKWebView*)webView {
    NSString* htmlPath = [[NSBundle mainBundle] pathForResource:@"ExampleApp" ofType:@"html"];
    NSString* appHtml = [NSString stringWithContentsOfFile:htmlPath encoding:NSUTF8StringEncoding error:nil];
    NSURL *baseURL = [NSURL fileURLWithPath:htmlPath];
    [webView loadHTMLString:appHtml baseURL:baseURL];
}
//WKWebView
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(WKNavigation *)navigation {
    NSLog(@"網頁開始載入");
}
- (void)webView:(WKWebView *)webView didFinishNavigation:(WKNavigation *)navigation {
    NSLog(@"網頁載入完成");
}
//UIWebView
//- (void)webViewDidStartLoad:(UIWebView *)webView {
//    NSLog(@"網頁開始載入");
//}
//- (void)webViewDidFinishLoad:(UIWebView *)webView {
//    NSLog(@"網頁載入完成");
//}

程式碼解釋就不說了,多執行檢查幾遍,自行理解與吸收

另外:
OC -> JS

// 單純的呼叫 JSFunction,不往 JS 傳遞引數,也不需要 JSFunction 的返回值。
[_bridge callHandler:@"testJavascriptHandler"];
// 呼叫 JSFunction,並向 JS 傳遞引數,但不需要 JSFunciton 的返回值。
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"提醒訊息":@"JS做好準備" }];
// 呼叫 JSFunction ,並向 JS 傳遞引數,也需要 JSFunction 的返回值。
[_bridge callHandler:@"testJavascriptHandler" data:@{ @"提醒訊息":@"JS做好準備" } responseCallback:^(id responseData) {
    NSLog(@"JS 的返回值: %@",responseData);
}];

JS -> OC

// JS 單純的呼叫 OC 的 block
WebViewJavascriptBridge.callHandler('shareClick');
// JS 呼叫 OC 的 block,並傳遞 JS 引數
WebViewJavascriptBridge.callHandler('shareClick',"JS 引數");
// JS 呼叫 OC 的 block,傳遞 JS 引數,並接受 OC 的返回值。
WebViewJavascriptBridge.callHandler('shareClick',{'foo': 'bar'},function(dataFromOC){
    alert("JS 呼叫了 OC 的分享方法!");
    document.getElementById("returnValue").value = dataFromOC;
});

上述所有程式碼在LJMElevateSelf in GitHub
Home資料夾下的BlocksKitViewController與WebViewJavascriptBridgeViewController;
html檔案在Networks檔案下。

參考連結:
WebViewJavascriptBridge in GitHub
Objective-C與JavaScript互動的那些事

相關文章