1. 程式人生 > >使用javascriptcore實現供h5呼叫的native介面

使用javascriptcore實現供h5呼叫的native介面

在app開發中使用webview,經常需要從js端呼叫和原生相關的互動功能。那麼這樣一層bridge的開發工作具體採用什麼方案來實現呢?
JS call OC:
方案1:
最古老也是使用最廣泛、且跨平臺的方案是在頁面內嵌入一個iframe,然後通過該iframe觸發的webview相關事件來進行hook,從而達到通訊的目的。
其中回撥方法的傳遞是通過生成一個id並儲存,來回傳遞id,在js端再通過id獲取到對應的fuction實現回撥。大名鼎鼎的cordova就是採用了這種方案實現了bridge。

方案2:
iOS7 蘋果引入了javascriptcore引擎;而該引擎可以用作js 和原生程式碼互動的橋樑。 那具體到webview裡面是怎樣實現的呢?
javascriptcore的使用,離不開的是jscontext。
對於UIWebview,我們可以在webview的代理方法(比如webViewDidFinishLoad)中使用如下程式碼獲取到jscontext並儲存:

    // Undocumented access to UIWebView's JSContext
    // TODO: base64 of documentView.webView.mainFrame.javaScriptContext
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

但是對於wkwebview,因為其內部實現的原因,我們無法獲取到jscontext,所以這裡我們不展開(在文章結尾處我們會大概說一下wkwebview可以採用的方案)。

UIwebview下實現供h5呼叫的native介面有兩種方式:
1. block
在webViewDidFinishLoad末尾插入如下程式碼(掃碼示例):

    @weakify_self;
    self.context[@"scanQRCode"] = ^(JSValue *cb)
    {
        @strongify_self;
        self.scanQRCB = cb;

        OrderCapture *capture = [[OrderCapture alloc] init];
        capture.scanType = OrderCaptureScanTypeAll
; capture.targetDelegate = self; [capture showDecodeView]; };

這裡的cb是js傳遞過來的回撥函式,通過scanQRCB這個屬性儲存了起來,後面在掃碼的delegate方法中可以通過它來呼叫回撥函式:

//條形碼返回結果
- (void)didFinishReader:(NSString *)value
{
    [self.scanQRCB callWithArguments:@[value]];
}

js呼叫的形式(注意:在window上直接呼叫):

<button onclick = "window.scanQRCode(callback)">點選我彈出原生的掃碼!</button>
  1. 通過JSExport協議包裝方法
    首先我們要為這些方法註冊一個共同的名稱空間了(這裡叫wq):
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
    // Undocumented access to UIWebView's JSContext
    // TODO: base64 of documentView.webView.mainFrame.javaScriptContext
    self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    // do the bridge below... here we use jsexport to do the bridge.

    self.context[@"wq"] = self;
}

實現原生的方法:

- (void)nativeAlert:(NSString *)title cb:(JSValue *)value
{
    self.alertCB = value;

    @weakify_self;
    dispatch_async(dispatch_get_main_queue(), ^{
        @strongify_self;
        self.alert = [[UIAlertView alloc] initWithTitle:title message:@""
                                               delegate:self
                                      cancelButtonTitle:@"取消"
                                      otherButtonTitles:nil, nil];
        [self.alert show];
    });

}

這裡的引數應該是和js呼叫時的順序對應,jsvalue可以對應js的function。

下面就是實現jsexport協議了,可以放在你的webview容器vc的.h最上面。

@protocol WqJSExport <JSExport>

JSExportAs
(openUrl,
 - (void)openUrlWithUrl:(NSString *)url title:(NSString *)title
 );

//- (void)nativeAlert:(NSString *)title;

JSExportAs
(nativeAlert,
 - (void)nativeAlert:(NSString *)title cb:(JSValue *)value
 );

@end

這裡實現了3個方法,分別演示了多引數、單引數、帶回調的export實現。
因js只支援單個引數,因此需要使用JSExportAs來對多引數的情況進行包裝。
如果只有一個引數,不需要用jsexportAs來包裝。
3個方法的呼叫示例:

<button onclick = "window.wq.openUrl(url, title)">通過原生開啟頁面!</button>

<button onclick = "window.wq.nativeAlert(biaoti, alertCallback)">點選我彈出原生的alert!</button>

<button onclick = "window.wq.nativeAlert(biaoti, alertCallback)">點選我彈出原生的alert!</button>

那麼這樣jscore在uiwebview上提供給js的bridge實現就講完了,這種方法的好處是實現非常清晰,且沒有額外的iframe開銷;不失為一種優雅的bridge解決方案。

而對於wkwebview來說,需要採用另外的方式來實現(window.webkit.messageHandlers.xxxMethod.postMessage),和上面的方法完全不同,就不再展開了。

而如果使用iframe的方案,可以同時在wkwebview和uiwebview上起作用,考慮同時支援兩種webview的情況下使用這種方案是比較合理的,無需做很多額外的處理;關於這套方案的具體實現,有時間再來細說一下(其實不復雜)。

補充:
對於wkwebview,不會自動彈出alert、prompt還有另外一個什麼來著,而是可以通過代理方法,需要處理好相應的代理方法才可以完成互動(別以為是bug了,哈哈)。
例(對於alert):

webView:runJavaScriptAlertPanelWithMessage:initiatedByFrame: