使用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>
- 通過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: