web-dev-qa-db-ja.com

WKWebViewへのJavaScript同期ネイティブ通信

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_の割り当て方法のみが異なります。直接割り当てる代わりに、空のsrciframe属性が使用されます。

例(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")    
}
_
29
paulvs

いいえ、WKWebViewのマルチプロセスアーキテクチャが原因で可能であるとは思いません。 WKWebViewはアプリケーションと同じプロセスで実行されますが、独自のプロセスで実行されるWebKitと通信します( Introducing the Modern WebKit API )。 JavaScriptコードはWebKitプロセスで実行されます。したがって、本質的には、設計に反する2つの異なるプロセス間で同期通信を行うことを求めています。

9
Martin Sherburn

私もこの問題を調査し、あなたとして失敗しました。回避策として、JavaScript関数をコールバックとして渡す必要があります。ネイティブ関数は、結果を返すためにコールバック関数を評価する必要があります。実際、JavaScriptは決して待機しないため、これはJavaScriptの方法です。 JavaScriptスレッドをブロックするとANRが発生する可能性があります。これは非常に悪いことです。

XWebView という名前のプロジェクトを作成しました。これにより、ネイティブとJavaScriptの間のブリッジを確立できます。 JavaScriptからネイティブを呼び出すためのバインディングスタイルのAPIを提供します。 sample アプリがあります。

3
soflare

同期通信を行うためのハックを見つけましたが、まだ試していません: https://stackoverflow.com/a/49474323/287078

編集:基本的に、JS Prompt()を使用して、ペイロードをjs側からネイティブ側に運ぶことができます。ネイティブWKWebViewでは、プロンプト呼び出しをインターセプトして、それが通常の呼び出しであるか、それともjsbridge呼び出しであるかを決定する必要があります。次に、結果をPromptコールへのコールバックとして返すことができます。プロンプト呼び出しは、ユーザー入力を待機するように実装されているため、JavaScriptネイティブの通信は同期されます。欠点は、通信できるのはトラフの文字列のみであることです。

2
h3dkandi

私は同様の問題に直面していましたが、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を呼び出します。

0
Ashwani Chandil