web-dev-qa-db-fra.com

Comment attendre un appel asynchrone en JavaScript dans une fonction synchrone?

J'ai récemment dû corriger des problèmes de sécurité dans une application Web (que je n'avais pas créée). Le problème de sécurité était qu'il utilisait des cookies non-http uniquement. J'ai donc dû définir le cookie de session http uniquement, ce qui signifie que vous ne pouvez plus lire (et définir) la valeur du cookie à partir de javascript. Jusque là, tellement facile à coudre.

Le problème plus profond était, l'application Web utilisée

JSON.parse(readCookie(cookieName)).some_value

sur un million de places.

Donc, afin de ne pas avoir à réécrire "un million de lignes de code", je devais créer un noeud final ajax qui me donnait le contenu du cookie http sous forme de code JSON et réécrire readCookie pour utiliser DEMANDES SYNCHRONES ajax (au lieu de lire le cookie), car le reste du code horrible s'attend à ce que readCookie soit synchrone à ces millions d'emplacements, car la lecture d'un cookie est synchrone.

Le problème est maintenant, je reçois beaucoup de

Synchronous XMLHttpRequest sur le thread principal est obsolète en raison de ses effets néfastes sur l'expérience de l'utilisateur final. Pour plus d’aide, consultez https://xhr.spec.whatwg.org/ .

qui spams la console de débogage, sans parler de la possibilité que quelqu'un décide de supprimer cette fonctionnalité.

Je cherche donc dans les nouveaux mots-clés async/wait de l'ES, pour voir si cela peut aider d'une manière ou d'une autre à faire une demande ajax asynchrone de manière synchrone (je sais que je dois utiliser des wrappers pour IE 11).

Jusqu'à présent, j'ai lu ces pages
https://www.twilio.com/blog/2015/10/asyncawait-the-hero-javascript-deserved.html
https://pouchdb.com/2015/03/05/taming-the-async-beast-with-es7.html
https://jakearchibald.com/2014/es7-async-functions/
https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Statements/function *

mais il semble que tous les nouveaux éléments asynchrones semblent uniquement résoudre le problème de l'écriture plus simple du code asynchrone, ne permettant pas l'interopérabilité entre le code asynchrone et le code synchrone existant. En utilisant les informations que j'ai lues, je peux maintenant attendre le résultat d'un appel ajax asynchrone comme s'il était synchrone, mais le problème est que l'attente est autorisée uniquement dans les méthodes asynchrones ... Ce qui signifie que même si je peux attendre le résultat, était synchrone, la méthode getCookie devait toujours être asynchrone, ce qui rendait tout le contenu totalement inutile (à moins que votre code ne soit entièrement asynchrone, ce qui n'est certainement pas le cas lorsque vous ne partez pas de zéro). .

Je n'arrive pas à trouver aucune information sur la façon d'interopérer entre code synchrone et asynchrone.

Par exemple, en C #, je peux appeler une méthode asynchrone à partir d'un contexte synchrone avec .Result, par ex.

 AsyncContext.RunTask(MyAsyncMethod).Result;

ou plus facile mais moins impasse-sûr comme

MyAsyncMethod(args).Result;

Est-il possible d'obtenir la même chose en JavaScript?

Il semble peu logique de propager de manière asynchrone lorsque le reste de la base de code est synchrone, sans aucune possibilité d'interopérabilité ... N'y a-t-il pas encore vraiment moyen de réaliser cela en JavaScript en 2017?

Je souligne encore:
Je sais comment je peux effectuer un appel ajax synchrone et je sais comment utiliser des appels asynchrones ajax avec des rappels et/ou des promesses.
Mais ce que je suis incapable de comprendre, c'est comment synchroniser un appel async-ajax (sans rappel) afin qu'il puisse être utilisé à partir d'un code censé être exécuté synchrone ( dans "un million d'endroits")!

C'est ce que j'ai essayé jusqu'à présent:
(Notez que j'utiliseloadQuoteoumain, le texte"Ron a déjà dit" apparaît toujours en premier dans la console de débogage, ce qui ne devrait pas être le cas) si l'appel asynchrone ajax avait été résolu de manière synchrone)

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml" lang="en">
<head>
    <meta http-equiv="X-UA-Compatible" content="IE=Edge,chrome=1" />

    <meta http-equiv="cache-control" content="max-age=0" />
    <meta http-equiv="cache-control" content="no-cache" />
    <meta http-equiv="expires" content="0" />
    <meta http-equiv="expires" content="Tue, 01 Jan 1980 1:00:00 GMT" />
    <meta http-equiv="pragma" content="no-cache" />

    <meta charset="utf-8" />
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />

    <meta http-equiv="Content-Language" content="en" />
    <meta name="viewport" content="width=device-width,initial-scale=1" />

    <meta name="google" value="notranslate" />


    <!--
    <meta name="author" content="name" />
    <meta name="description" content="description here" />
    <meta name="keywords" content="keywords,here" />

    <link rel="shortcut icon" href="favicon.ico" type="image/vnd.Microsoft.icon" />
    <link rel="stylesheet" href="stylesheet.css" type="text/css" />
    -->

    <title>Title</title>

    <style type="text/css" media="all">
        body
        {
            background-color: #0c70b4;
            color: #546775;
            font: normal 400 18px "PT Sans", sans-serif;
            -webkit-font-smoothing: antialiased;
        }
    </style>


    <script type="text/javascript">
        <!-- 
        // http://localhost:57566/foobar/ajax/json.ashx

        var ajax = {};
        ajax.x = function () {
            if (typeof XMLHttpRequest !== 'undefined') {
                return new XMLHttpRequest();
            }
            var versions = [
                "MSXML2.XmlHttp.6.0",
                "MSXML2.XmlHttp.5.0",
                "MSXML2.XmlHttp.4.0",
                "MSXML2.XmlHttp.3.0",
                "MSXML2.XmlHttp.2.0",
                "Microsoft.XmlHttp"
            ];

            var xhr;
            for (var i = 0; i < versions.length; i++) {
                try {
                    xhr = new ActiveXObject(versions[i]);
                    break;
                } catch (e) {
                }
            }
            return xhr;
        };

        ajax.send = function (url, callback, method, data, async) {
            if (async === undefined) {
                async = true;
            }
            var x = ajax.x();
            x.open(method, url, async);
            x.onreadystatechange = function () {
                if (x.readyState == 4) {
                    callback(x.responseText)
                }
            };
            if (method == 'POST') {
                x.setRequestHeader('Content-type', 'application/x-www-form-urlencoded');
            }
            x.send(data)
        };

        ajax.get = function (url, data, callback, async) {
            var query = [];
            for (var key in data) {
                query.Push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
            }
            ajax.send(url + (query.length ? '?' + query.join('&') : ''), callback, 'GET', null, async)
        };

        ajax.post = function (url, data, callback, async) {
            var query = [];
            for (var key in data) {
                query.Push(encodeURIComponent(key) + '=' + encodeURIComponent(data[key]));
            }
            ajax.send(url, callback, 'POST', query.join('&'), async)
        };


        ///////////



        function testAjaxCall() {
            ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus)
                {
                    console.log("args:", arguments);

                    console.log("Error:", bError);
                    console.log("Message:", strMessage);
                    console.log("Status:", iStatus);
                }
                , true
            );

        }
        -->
    </script>

</head>
<body>

    <script type="text/javascript">

        function getQuote() {
            var quote;

            return new Promise(function (resolve, reject) {

                ajax.get("./ajax/json.ashx", null, function (bError, strMessage, iStatus) {

                    // console.log("args:", arguments);

                    // console.log("Error:", bError);
                    // console.log("Message:", strMessage);
                    // console.log("Status:", iStatus);


                    quote = bError;
                    resolve(quote)

                }, true);


                /*
                request('./ajax/json.ashx', function (error, response, body) {
                    quote = body;

                    resolve(quote);
                });
                */

            });

        }

        async function main() {
            var quote = await getQuote();
            console.log("quote: ", quote);
        }

        function myGetQuote() {
            var quote = async function () { return await getQuote(); };

            console.log("quote: ", quote);

            return quote;
        }

        function spawn(generatorFunc) {
            function continuer(verb, arg) {
                var result;
                try {
                    result = generator[verb](arg);
                } catch (err) {
                    return Promise.reject(err);
                }
                if (result.done) {
                    return result.value;
                } else {
                    return Promise.resolve(result.value).then(onFulfilled, onRejected);
                }
            }
            var generator = generatorFunc();
            var onFulfilled = continuer.bind(continuer, "next");
            var onRejected = continuer.bind(continuer, "throw");
            return onFulfilled();
        }


        function loadQuote() 
        {
            return spawn(function *() {
                try {
                    let story = yield getQuote();

                    console.log("story:", story);
                    // addHtmlToPage(story.heading);
                    // for (let chapter of story.chapterURLs.map(getJSON)) { addHtmlToPage((yield chapter).html); } addTextToPage("All done");
                } catch (err) {
                    //addTextToPage("Argh, broken: " + err.message);
                    console.log("Argh, broken: " + err.message);
                }
                //document.querySelector('.spinner').style.display = 'none';
            });
        }



        function autorun()
        {           
            console.clear();    
            // main();
            // main();
            loadQuote();

            //var quote = myGetQuote();

            // console.log("quote: ", quote);
            console.log('Ron once said,');

        }

        if (document.addEventListener) document.addEventListener("DOMContentLoaded", autorun, false);
        else if (document.attachEvent) document.attachEvent("onreadystatechange", autorun);
        else window.onload = autorun;
    </script>

</body>
</html>
6
Stefan Steiger

mais le problème est que l'attente n'est autorisée que dans les méthodes asynchrones.

Exactement, et non, il n'y a pas de solution de contournement pour cela. La sémantique javascript demand complète de l'exécution des fonctions synchrones avant l'exécution de toute action asynchrone en attente (telle que le rappel à un gestionnaire XHR pour un appel XHR asynchrone) peut être exécutée.

JavaScript s'exécute sur un thread donné en traitant une file d'attente de travaux1:

  1. Ramasser le prochain travail en attente
  2. Exécuter le code pour ce travail de manière synchrone
  3. Ce n'est que lorsque ce travail est terminé que vous revenez à l'étape 1 pour passer au travail suivant.

(C'est un peu plus compliqué que cela, il y a deux niveaux, mais ce n'est pas pertinent pour cette question particulière.)

Les tâches XHR et autres sont des tâches planifiées dans la file d'attente. Il n'y a aucun moyen de suspendre un travail, d'exécuter un autre travail à partir de la file d'attente et de reprendre le travail en pause. asyncawait fournit une syntaxe syntax considérablement plus simple pour la gestion des opérations asynchrones, mais ne modifie pas la nature de la file d'attente.

La seule solution que je vois dans votre situation est d’aller de façon asynchrone jusqu’au niveau le plus élevé. Cela peut ne pas être aussi compliqué que vous pourriez le penser (ou peut-être que ce sera le cas). Dans de nombreux cas, il ajoute async devant function dans de nombreuses fonctions. Cependant, rendre ces fonctions asynchrones aura probablement des effets d'entraînement importants (par exemple, quelque chose qui était synchrone dans un gestionnaire d'événements devenant asynchrone modifie le minutage de ce qui se passe par rapport à l'interface utilisateur).

Par exemple, considérons ce code synchrone:

var btn = document.getElementById("btn");

btn.addEventListener("click", handler, false);

function handler(e) {
  console.log("handler triggered");
  doSomething();
  console.log("handler done");
}

function doSomething() {
  doThis();
  doThat();
  doTheOther();
}

function doThis() {
  console.log("doThis - start & end");
}
function doThat() {
  console.log("doThat - start");
  // do something that takes a while
  var stop = Date.now() + 1000;
  while (Date.now() < stop) {
    // wait
  }
  console.log("doThat - end");
}
function doTheOther() {
  console.log("doThat - start & end");
}
.as-console.wrapper {
  max-height: 80% !important;
}
<input type="button" id="btn" value="Click Me">
<p id="text"></p>

Désormais, nous souhaitons rendre doThat async (_/note: ne fonctionnera que sur un navigateur récent prenant en charge async/await, comme Chrome; malheureusement, la configuration Babel de Stack Snippet ne les inclut pas, nous ne pouvons donc pas utiliser cette option). :

var btn = document.getElementById("btn");

btn.addEventListener("click", handler, false);

// handler can't be async
function handler(e) {
  console.log("handler triggered");
  doSomething();
  console.log("handler done");
}

// doSomething can be
async function doSomething() {
  doThis();
  await doThat();
  doTheOther();
}

function doThis() {
  console.log("doThis - start & end");
}

// make doThat async
async function doThat() {
  console.log("doThat - start");
  // simulate beginning async operation with setTimeout
  return new Promise(resolve => {
    setTimeout(() => {
      // do something that takes a while
      var stop = Date.now() + 1000;
      while (Date.now() < stop) {
        // wait
      }
      console.log("doThat - end (async)");
    }, 0);
  });
}
function doTheOther() {
  console.log("doThat - start & end");
}
.as-console.wrapper {
  max-height: 80% !important;
}
<input type="button" id="btn" value="Click Me">
<p id="text"></p>

L'essentiel est qu'il soit asynchrone dès que possible, dans doSomething (puisque handler ne peut pas être async). Mais bien sûr, cela change le timing du travail par rapport au gestionnaire. (Bien entendu, nous aurions probablement dû mettre à jour handler pour détecter les erreurs de la promesse `doSomething ().]


1 C'est la terminologie de la spécification JavaScript. La spécification HTML5 (qui traite également de cela) les appelle "tâches" au lieu de "travaux".

5
T.J. Crowder

Il y a un problème avec votre approche. Premièrement, pour qu'une partie du code soit à await afin que l'opération async se termine, il doit être encapsulé dans une fonction async.

Par exemple:

async function asyncExample () {
    try {
        const response = await myPromise()

        // the code here will wait for the 
        // promise to fullfil
    } catch (error) {
        // the code here will execute if the promise fails
    }
}

function nonAsyncExample () {
    asyncExample () 

    console.log('this will not wait for the async to finish')
    // as it's not wrapped in an async function itself
}

Vous pouvez essayer de déclarer la fonction autorun() en tant que async, mais cela peut entraîner des complications supplémentaires.

Ma suggestion, si votre application JS a un point d’entrée, il est déclenché par un événement onload. Essayez de faire votre appel ajax avant ce point, puis stockez-le localement dans une variable et interrogez-le à partir de là.

Par exemple, si votre code ressemble à:

function init () {
    // perform initialisations here
}

document.addEventListener("DOMContentLoaded", init)

changer cela pour être

document.addEventListener("DOMContentLoaded", function () {
    getAjaxConfig().then(function (response) {
        window.cookieStash = response
        init()
    }
})

et récupérez vos données à partir de la variable cookieStash dans le reste de l'application. Vous n'aurez pas besoin d'attendre autre chose.

2
motanelu

Réponse courte: non, il n'y a aucun moyen de faire en sorte que le code asynchrone soit exécuté de manière synchrone dans JS, comme vous le connaissez depuis C #. Rendre tout asynchrone est une solution possible.

Cependant, étant donné que vous contrôlez également le côté serveur, j'ai une autre suggestion (un peu de piratage): envoyez le long des informations requises (contenu du cookie) sous forme de métadonnées de la demande, par exemple. comme balise méta HTML pour les demandes de page ou en-tête de réponse HTTP pour les demandes XHR, et stockez-le quelque part.

1
Lucero