JavaScriptとSwift/Obj-Cネイティブコード間の同期通信は、WKWebViewを使用して可能ですか?
これらは私が試し、失敗したアプローチです。
アプローチ1:スクリプトハンドラーの使用
WKWebView
のJSメッセージを受信する新しい方法は、デリゲートメソッド_userContentController:didReceiveScriptMessage:
_を使用することです。これはwindow.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?')
によってJSから呼び出されます。このアプローチの問題は、ネイティブデリゲートメソッドの実行中に、 JSの実行はブロックされないため、webView.evaluateJavaScript("something = 42", completionHandler: nil)
をすぐに呼び出して値を返すことはできません。
例(JavaScript)
_var something;
function getSomething() {
window.webkit.messageHandlers.myMsgHandler.postMessage("What's the meaning of life, native code?"); // Execution NOT blocking here :(
return something;
}
getSomething(); // Returns undefined
_
例(Swift)
_func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
}
_
アプローチ2:カスタムURLスキームの使用
JSでは、_window.location = "js://webView?hello=world"
_を使用してリダイレクトすると、ネイティブのWKNavigationDelegate
メソッドが呼び出され、URLクエリパラメータを抽出できます。ただし、UIWebViewとは異なり、デリゲートメソッドはJSの実行をブロックしていないため、すぐにevaluateJavaScript
を呼び出して値をJSもここでは機能しません。
例(JavaScript)
_var something;
function getSomething() {
window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
return something;
}
getSomething(); // Returns undefined
_
例(Swift)
_func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
webView.evaluateJavaScript("something = 42", completionHandler: nil)
decisionHandler(WKNavigationActionPolicy.Allow)
}
_
アプローチ3:カスタムURLスキームとIFRAMEを使用する
このアプローチは、_window.location
_の割り当て方法のみが異なります。直接割り当てる代わりに、空のsrc
のiframe
属性が使用されます。
例(JavaScript)
_var something;
function getSomething() {
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?hello=world");
document.documentElement.appendChild(iframe); // Execution NOT blocking here either :(
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething();
_
それでも、これは解決策ではありません。同期ではないアプローチ2と同じネイティブメソッドを呼び出します。
付録:これを古いUIWebViewで実現する方法
例(JavaScript)
_var something;
function getSomething() {
// window.location = "js://webView?question=meaning" // Execution is NOT blocking if you use this.
// Execution IS BLOCKING if you use this.
var iframe = document.createElement("IFRAME");
iframe.setAttribute("src", "js://webView?question=meaning");
document.documentElement.appendChild(iframe);
iframe.parentNode.removeChild(iframe);
iframe = null;
return something;
}
getSomething(); // Returns 42
_
例(Swift)
_func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
webView.stringByEvaluatingJavaScriptFromString("something = 42")
}
_
いいえ、WKWebViewのマルチプロセスアーキテクチャが原因で可能であるとは思いません。 WKWebViewはアプリケーションと同じプロセスで実行されますが、独自のプロセスで実行されるWebKitと通信します( Introducing the Modern WebKit API )。 JavaScriptコードはWebKitプロセスで実行されます。したがって、本質的には、設計に反する2つの異なるプロセス間で同期通信を行うことを求めています。
私もこの問題を調査し、あなたとして失敗しました。回避策として、JavaScript関数をコールバックとして渡す必要があります。ネイティブ関数は、結果を返すためにコールバック関数を評価する必要があります。実際、JavaScriptは決して待機しないため、これはJavaScriptの方法です。 JavaScriptスレッドをブロックするとANRが発生する可能性があります。これは非常に悪いことです。
XWebView という名前のプロジェクトを作成しました。これにより、ネイティブとJavaScriptの間のブリッジを確立できます。 JavaScriptからネイティブを呼び出すためのバインディングスタイルのAPIを提供します。 sample アプリがあります。
同期通信を行うためのハックを見つけましたが、まだ試していません: https://stackoverflow.com/a/49474323/287078
編集:基本的に、JS Prompt()を使用して、ペイロードをjs側からネイティブ側に運ぶことができます。ネイティブWKWebViewでは、プロンプト呼び出しをインターセプトして、それが通常の呼び出しであるか、それともjsbridge呼び出しであるかを決定する必要があります。次に、結果をPromptコールへのコールバックとして返すことができます。プロンプト呼び出しは、ユーザー入力を待機するように実装されているため、JavaScriptネイティブの通信は同期されます。欠点は、通信できるのはトラフの文字列のみであることです。
私は同様の問題に直面していましたが、promiseコールバックを保存することで解決しました。
WKUserContentController :: addUserScriptを介してWebビューにロードするjs
var webClient = {
id: 1,
handlers: {},
};
webClient.onMessageReceive = (handle, error, data) => {
if (error && webClient.handlers[handle].reject) {
webClient.handlers[handle].reject(data);
} else if (webClient.handlers[handle].resolve){
webClient.handlers[handle].resolve(data);
}
delete webClient.handlers[handle];
};
webClient.sendMessage = (data) => {
return new Promise((resolve, reject) => {
const handle = 'm' + webClient.id++;
webClient.handlers[handle] = { resolve, reject };
window.webkit.messageHandlers.<message_handler_name>.postMessage({data: data, id: handle});
});
}
のようなJSリクエストを実行する
webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});
UserContentController:didReceiveScriptMessageでリクエストを受信する
WebClient.onMessageReceive(handle、error、response_data)でevaluateJavaScriptを呼び出します。