web-dev-qa-db-fra.com

Communication native synchrone JavaScript avec WKWebView

La communication synchrone entre JavaScript et le code natif Swift/Obj-C est-elle possible à l'aide de WKWebView?

Ce sont les approches que j'ai essayées et qui ont échoué.

Approche 1: utilisation des gestionnaires de script

WKWebView la nouvelle façon de recevoir des messages JS est d'utiliser la méthode déléguée userContentController:didReceiveScriptMessage: qui est invoquée depuis JS par window.webkit.messageHandlers.myMsgHandler.postMessage('What's the meaning of life, native code?') Le problème avec cette approche est que lors de l'exécution du délégué natif , l'exécution de JS n'est pas bloquée, nous ne pouvons donc pas retourner de valeur en appelant immédiatement webView.evaluateJavaScript("something = 42", completionHandler: nil).

Exemple (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

Exemple (Swift)

func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
}

Approche 2: utilisation d'un schéma d'URL personnalisé

Dans JS, la redirection à l'aide de window.location = "js://webView?hello=world" Appelle les méthodes natives WKNavigationDelegate, où les paramètres de requête URL peuvent être extraits. Cependant, contrairement à UIWebView , la méthode déléguée ne bloque pas l'exécution JS, donc appelez immédiatement evaluateJavaScript pour renvoyer une valeur à la JS ne fonctionne pas non plus ici.

Exemple (JavaScript)

var something;
function getSomething() {
    window.location = "js://webView?question=meaning" // Execution NOT blocking here either :(
    return something;
}
getSomething(); // Returns undefined

Exemple (Swift)

func webView(webView: WKWebView, decidePolicyForNavigationAction navigationAction: WKNavigationAction, decisionHandler decisionHandler: (WKNavigationActionPolicy) -> Void) {
    webView.evaluateJavaScript("something = 42", completionHandler: nil)
    decisionHandler(WKNavigationActionPolicy.Allow)
}

Approche 3: utilisation d'un schéma d'URL personnalisé et d'un IFRAME

Cette approche ne diffère que par la façon dont window.location Est attribué. Au lieu de l'attribuer directement, l'attribut src d'un iframe vide est utilisé.

Exemple (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();

Ce n'est néanmoins pas une solution non plus, elle invoque la même méthode native que l'Approche 2, qui n'est pas synchrone.

Annexe: comment y parvenir avec l'ancien UIWebView

Exemple (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

Exemple (Swift)

func webView(webView: UIWebView, shouldStartLoadWithRequest request: NSURLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    webView.stringByEvaluatingJavaScriptFromString("something = 42")    
}
29
paulvs

Non, je ne pense pas que cela soit possible en raison de l'architecture multi-processus de WKWebView. WKWebView s'exécute dans le même processus que votre application mais il communique avec WebKit qui s'exécute dans son propre processus ( Présentation de l'API Modern WebKit ). Le code JavaScript s'exécutera dans le processus WebKit. Donc, vous demandez essentiellement une communication synchrone entre deux processus différents, ce qui va à l'encontre de leur conception.

9
Martin Sherburn

J'ai également enquêté sur ce problème et j'ai échoué comme vous. Pour contourner ce problème, vous devez passer une fonction JavaScript en tant que rappel. La fonction native doit évaluer la fonction de rappel pour renvoyer le résultat. En fait, c'est la manière JavaScript, car JavaScript n'attend jamais. Le blocage du fil JavaScript peut provoquer l'ANR, c'est très mauvais.

J'ai créé un projet nommé XWebView qui peut établir un pont entre natif et JavaScript. Il propose une API de style contraignant pour appeler natif à partir de JavaScript et vice versa. Il existe une application exemple .

3
soflare

J'ai trouvé un hack pour faire une communication synchrone mais je ne l'ai pas encore essayé: https://stackoverflow.com/a/49474323/287078

Edit: Fondamentalement, vous pouvez utiliser le JS Prompt () pour transporter votre charge utile du côté js vers le côté natif. Dans le natif, WKWebView devra intercepter l'appel d'invite et décider s'il s'agit d'un appel normal ou s'il s'agit d'un appel jsbridge. Vous pouvez ensuite renvoyer votre résultat en tant que rappel à l'appel rapide. Parce que l'appel d'invite est implémenté de telle manière qu'il attend l'entrée utilisateur, votre communication native javascript sera synchrone. L'inconvénient est que vous ne pouvez communiquer que des chaînes de creux.

2
h3dkandi

J'étais confronté à un problème similaire, je l'ai résolu en stockant les rappels de promesses.

Les js que vous chargez dans votre vue Web via WKUserContentController :: addUserScript

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});
   });
}

Exécuter la requête Js comme

webClient.sendMessage(<request_data>).then((response) => {
...
}).catch((reason) => {
...
});

Recevoir une demande dans userContentController: didReceiveScriptMessage

Appelez assessJavaScript avec webClient.onMessageReceive (handle, error, response_data).

0
Ashwani Chandil