Ajax: async or not async? callbacks, promesses

Bonjour.

Je lis partout, et surtout aussi en log F12, que async=false, par défaut dans le temps il me semble, devrait être abandonné car ça peut bloquer les clients et ralentir les serveurs.

Mais c’est bien gentil tout ça mais pour une page où il y un ou 2 appels ajax ça va, mais si j’ai 20 appels ajax, dans un tableau par exemple, et que je ne veux pas que ça me ramène la ligne du voisin je suis obligé de mettre un timeout d’au moins 2 secondes avec async=true. Certes ça ne bloque pas le navigateur mais l’utilisateur il attend quand même.

Je n’ai trouvé aucune réponse satisfaisante même pas dans la page longue comme le bras, et un peu confuse, de Mozilla.

La solution serait les workers, mais j’ai peut être ai-je mal lu ? Est-ce quelqu’un aurait un lien à ce sujet ? Pour moi en théorie async=true pourrait fonctionner qu’avec des identifiants uniques, sinon je ne vois pas comment il ne peut pas récupérer un appel d’une autre ligne.

Est-ce qu’il existe une page non pas sur les workers en général mais sur la problématique en particulier d’appel ajax asynchrone avec les workers, ou tout autre solution ou de fait on a besoin de quelque chose similaire à la fonctionnalité d’être synchrone ?

Après c’est davantage pour progresser car mes async=false fonctionnent très bien.

En gros dans un navigateur ne pas mettre les fichiers css ou js en sync bloque le rendu en effet, d’où la recommandation notamment pour le SEO de tout mettre en async comme ça le rendu du html n’est pas bloqué par le chargement des scripts ou du style (c’est également encouragé par http2).

Si j’ai bien compris (je pense pas mais bon :p), ce dont tu as besoin c’est d’enchainer plusieurs fonctions asynchrones les unes à la suite des autres dès le moment où l’user charge la page, afin de construite un objet que tu vas ensuite afficher sous forme de tableau ?

Dans ce cas l’idéal serait peut-être de faire des async/await ? Note cependant que faire 20 requêtes pour peupler un tableau ne me semble pas une super bonne pratique. Tu ne peux pas en faire une qui pointe vers un controller qui va s’occuper de regrouper toutes tes données pour ensuite te renvoyer ton tableau complet ?

Merci pour ton aide :slight_smile:

Oui et non.

En fait mon tableau est construit de la façon dont tu parles, par R Shiny mais cela aurait pu être n’importe quel autre type de serveur.

Mais je veux que quand je clique sur un ⊕ sur une des lignes, alors se crée une ligne dépliable en dessous ça déplie du bla et éventuellement un sous tableau et ceci à la demande simplement. Et je ne cherche pas du tout à ce que ça soit asynchrone : les bonnes pratiques récentes me disent que ça doit être asynchrone et je cherche à m’y conformer mais sans succès. En mode asynchrone il y a un décalage et le timeout est trop long pour l’utilisabilité.

Mais comme mon utilisateur veut 50 colonnes de données, et qu’en plus je voudrais qu’une partie d’entre elles n’apparaissent que dans un sous-tableau dépliant, hé ben je lui fournis à la demande ces 3 ou 4 lignes. D’ailleurs si j’en suis venu à cette solution ajax synchrone c’est que juste avant j’avais aussi utilisé un tableau dépliant pré-calculé, mais dont le precalcul dure 10 minutes à cause des consolidations que je fais côté serveur. Précalculs métiers que je lance par batch régulièrement.

Là mes données sont rapides à aller chercher à la demande et pas besoin d’être consolidées(*) .

J’ai regardé ton lien mais je ne vois pas d’ajax et il me semble que les promesses n’existent pas en vanilla js. Je cherche à ce que ça fonctionne ou en jquery ou en vanilla js car je ne veux pas installer de transpiler avec R Shiny.

Actuellement tout fonctionne bien en synchrone. Je cherchais simplement à me conformer aux bonnes pratiques récentes. Mais s’il faut que j’installe babel, non merci :slight_smile: ou alors il va falloir de solides arguments pour 30 lignes en js sur des centaines en R Shiny.

C’est en intranet.

Edit : *= par contre elles alourdissent inutilement mon tableau si je les calcule une fois pour toutes.

Ah oui t’es sur un use case vraiment pas commun et une UX… compliquée :laughing:. Alors premièrement l’async await est totalement du javascript natif que tu peux utiliser dans ton navigateur si celui-ci supporte ES2017. Tu pourras trouver le détail du support navigateur ici pour async et ici pour await. Pour les requêtes la plupart des devs JS utilisent Fetch pour les requêtes.

J’espère que tu ne dois pas supporter IE11 mais si ça devait être le cas tu peux utiliser axios via un simple script pour faire de l’async/await, ça peut être une solution.

Sinon si tu as déjà jquery et que tu ne veux pas une ressource en plus et que tu dois pas supporter ie11, apparemment c’est possible aussi.

Pour le reste je pense malheureusement que si la solution de d’enchaîner une série de requêtes asynchrone ne fonctionne pas, tu vas devoir faire tes requêtes synchrones. Désolé je suis pas super fort pour tout ce qui est requests, je fais surtout de l’UI. :confused:

Si tu n’as pas de contraintes sur la compatibilité avec IE11, effectivement les promesses sont une bonne solution pour toi.

Si tu veux tu peux me passer un bout de code pour que j’essaie de te montrer comment faire pour l’adapter pour les promesses.

Ah intéressant et concis cet article.

Après j’ai vu dans un de tes liens ou sous-liens, pour avoir une version exacte de support.

« NOTE: async/await is part of ECMAScript 2017 and is not supported in Internet Explorer and older browsers, so use with caution.»

Ha merci je prendrais cela au boulot.

On a du Windows 7 mais avec du Firefox récent, mais aussi du Mac / Safari avec des versions que j’ignore. Ces derniers semblent avoir des comportements bizarre qu’on ne voit sous Firefox. Pour les IE sous Windows je leur demande à passer sous Firefox.

Je ne comprends pas trop ce qui cloche avec de l’asynchrone niveau utilisabilité, si tu affiches un truc de chargement (genre bidule qui tourne) pendant que les résultats sont récupérés du serveur.

En synchrone, ton code devrait ressembler à ça:

onClick: function(contexte) {
 let reponse = <appel synchrone()>
 renduDeLaReponse(reponse, contexte);
}

Pour le passer en asynchrone, tu peux faire un truc comme ça:

onClick: function(contexte) {
  afficherPatienter(contexte);
  appelAsynchrone(function(reponse) {
    enleverPatienter(contexte);
    renduDeLaReponse(reponse, contexte);
  }
}

Le risque avec de l’asynchone en vanillaJS (voire JQuery) c’est le « callback hell », ainsi que la gestion du statut des différents éléments de la UI quand on rajoute la gestion des erreurs des appels async par dessus - il vaut mieux créer un controlleur pour s’y retrouver.

Ma réponse a l’air vachement simpliste du coup j’ai vraiment l’impression d’avoir loupé un truc… Tout comme Mistermick, un exemple du code pourrait aider.

Oui ton code est concis mais c’est justement un des gros avantages de async/await par rapport à avant, on se retrouve avec un truc super clean et simple au point que ça en devient suspect. :stuck_out_tongue:

Je n’utilise pas async/await, c’est du JS/JQuery de base. Tous les appels Ajax (asynchrones) prennent une fonction callback en paramètre qui est appelée lorsque le résultat est retourné.

Ne t’en fais pas, quand tu auras ajouté tout ton code de gestion des erreurs avec update des éléments de la UI en fonction, ça ne sera plus suspect du tout :slight_smile:

J’utilise cette méthode en datatable (1) que je modifie légèrement pour Shiny(2) comme indiqué .

J’ignore si 3) fonctionne avec les (+) dépliants dans les datatables DT shiny.

Après je ne voulais pas parler de tout ça en tout cas dans mon 1er message pour ne pas embrouiller.

En fait il n’affiche rien, et si je change de ligne il affiche la précédente ligne. Si je met un timeout ça marche mais je ne peux pas faire attendre 2 secondes à chaque clic. Et en dessous de 2 secondes, même symptôme. C’est pour cela que suis passé en synchrone où tout marche nickel sans attendre 2 secondes.

Je précise que les clics ne sont pas un déroulement d’événements toujours dans le même ordre, là ça marcherait sans doute en asynchrone, mais complètement aléatoire selon les souhaits de l’utilisateur dans la table.

Si il fait ça c’est que ton code a un problème. Tu devrais stocker un contexte quelque part qui te permet de savoir où le résultat de ton code async doit être affiché. Genre le numéro de ligne dans ta table. Ou un ID de div unique si tu as ça qui traîne. Tu peux récupérer ça soit à partir de l’object passé en paramètre dans le handler du click (i.e. l’object cliqué), soit si possible en passant un identifiant toi même à la fonction si tu gènères le code JS qui est appelé quand l’utilisateur clique sur le « plus ».

Ouais mais là tu triches, en synchrone tu empêche les utilisateurs de cliquer sur une autre ligne pendant que tu récupères les infos du serveur, c’est pas du tout une bonne solution :slight_smile:

L’idée c’est vraiment que quand un utilisateur clique sur un élément, un évenement indépendant est pris en compte, dans son propre contexte (sa ligne dans le tableau dans ton cas). Libre à l’utilisateur de cliquer sur plein de lignes dans l’ordre qu’il veut ensuite - tant que le code pour chaque clic ne déborde pas de son contexte, les différents lignes ne se marcheront pas sur les pieds et tout devrait tourner nickel.

C’est le cas, j’ai cet id car je m’en sers comme paramètre ajax pour que la fonction côté serveur renvoie les éléments correspondants à cette clé.

Oui je comprends bien la mauvaise pratique mais dans les faits il n’est pas bloqué… sauf si le serveur a des vapeurs ce qui n’arrive que si j’ai un bug.

C’est pour cela que je pose la question :slight_smile:

Ha oui?

Comment ça ?

En tout cas l’idéal est que je change quelques lignes de js pour que ça marche en async…tout en restant compatible avec Safari / Mac et sans installer un transpiler.

Non, la UI est bloquée dans le cas d’un appel synchrone. À moins d’utiliser des web workers, il n’y a qu’un thread qui tourne dans ton browser (oui bon pas vraiment mais pour simplifier on va dire que si :slight_smile: ). Du coup si tu as disons un autre bouton ailleurs qui affiche un message « hello » quand tu cliques dessus, il ne va rien se passer si l’utilisateur clique dessus alors que ton appel sync est en cours - et ça c’est considéré comme inacceptable en terme d’expérience utilisateur. Alors je sais bien que tu as un timeout de 2 secondes et que ça peut paraître « acceptable » en terme d’expérience utilisateur, mais en fait il faut vraiment éviter.

Ben tu dis que tu as déjà je contexte pour faire l’appel Ajax (genre le numéro de ligne). Donc comment est ton code JS qui met à jour le contenu de la ligne avec le retour de l’appel ajax? Est-ce qu’il vise bien la bonne ligne? Est-ce que tu n’utiliserais pas par hasard des variables globales pour stocker le resultat des appels Ajax ou la valeur de contexte (numéro de ligne) utilisée pour faire l’appel ajax?

Après c’est peut-être lié à la lib que tu utilises. Je ne connais ni Shiny, ni Datatables donc c’est dur de savoir d’où viens le problème. Tu peux partager ton code qui update la UI avec le résultat de l’appel Ajax?

Tout à fait d’accord. Comme 2 secondes c’est inacceptable c’est pour cela que je suis passé en synchrone où je n’ai aucune attente.

Je ne pourrais le recopier que mercredi mais en attendant c’est similaire à ça


    var callback = (function($api) {
        return function() {
            var tr = $(this).parent();
            var row = $api.row(tr);
            if (row.child.isShown()) {
                row.child.hide();
                tr.removeClass('shown');
            }
            else {
                // we can use the unique ajax request URL to get the extra information.
                $.ajax(_ajax_url, {
                  data: {name: row.data()[1]},
                  success: function(res) { 
                      row.child(format(res)).show(); 
                      tr.addClass('shown');
                  }
                });
            }
        }

Issue de jquery - Shiny datatable with child rows using ajax - Stack Overflow d’où je suis parti je crois à l’origine mais en précisant async=false sinon j’avais les symptômes dont je parle plus haut.

Et donc j’envoie cela data: {name: row.data()[1]} comme clé. (où 1 est le numéro de colonne de la clé).

À mon avis le problème pourrait être là. Ça va dépendre du code qu’il y a autour, mais il est possible que cela point vers le dernier élément cliqué par l’utilisateur dans la chaîne JQuery, et donc ça va changer à chaque fois que l’utilisateur clique sur un truc.

Du coup tu peux essayer de déclarer une autre variable définie par $(this) avant la déclaration du callback pour stocker cet objet, genre:
let _that = $(this);
Et ensuite tu peux utiliser _that dans ton callback et tu es sûr de taper sur la bonne ligne puisque l’objet désigné par _that ne changera plus même si l’utilisateur clique sur une autre ligne.

Ah ok merci :grinning:
Je vais essayer cela mercredi.

Doonc. En fait le problème ne vient pas de là mais ça me met sur la bonne piste, effectivement j’ai un souci dans mon code. Quand je fais un console.log() au cœur de la fonction ajax j’ai en fait les bonnes données, mais c’est après que ça se gâte.

Notamment j’avais mal compris cette fonction
https://datatables.net/reference/api/row().child()

Mais maintenant je suis en train de voir mes soucis ajax.

En relisant les échanges, je suis d’accord avec deneb.

Si ça marche quand tu mets en synchrone et pas en asynchrone c’est qu’il y a quelque chose que tu utilises quand tu mets à jour ton tableau qui a changé quand tu as fait le second clic, très probablement quelque chose que tu utilises dans « success ».

Est-ce que tu as bien vérifié toutes les variables qui existent dans success ? notament « row » et « tr », si ces variables correspondent à ton deuxième click ça provoquerait l’erreur que tu décrit.

1 « J'aime »

J’ai progressé mais je suis bloqué. Voici mes codes, simplifiés.

Pour préciser, ce n’est pas la première fois que j’utilise ajax, en revanche c’est la première fois que je cherche à récupérer une variable venant d’ajax pour l’afficher plus tard. D’ailleurs il y a peu ou pas d’exemples sur ce domaine dans stackoverflow sauf sur les promesses en javascript récent et zappant le côté ajax.

Tout d’abord la partie en R. Je présente cette partie pour dire que je ne peux pas mettre de js au dessus. (En réalité si, mais je voudrais éviter d’en mettre partout).

DT::datatable(
  data=mesdonnees(),
  callback = JS("   // mon code JS "
)

Voici le code js d’appel en on click:

table.on('click', 'td.details-control', function () {
	// A priori td et row fonctionnent puisque je vois bien la bonne valeur dans le console.log de ajaxFn ()
	var td = $(this),
	  row = table.row(td.closest('tr'));
	if (row.child.isShown()) {
	  row.child.hide();
	  td.html('&oplus;');
	} else {
	  setTimeout(function() {
		//-- 1 --- chargement_ajax() est bien  appelé. J'avais appelé 2 fois la fonction child() de datatables    de suite, ce n'était pas bon mais ce n'était pas en cause car ça continuait à fonctionner...en synchrone. De toute façon j'ai corrigé mon erreur.
		row.child(chargement_ajax(row.data())).show();
		td.html('&CircleMinus;');
	  }, 1);  // avec des timeout de 2000 ms (2 sec) ça fonctionne en asynchrone, mais c'est insupportable
	}
})

Et le code appelé par le code précédent


var chargement_ajax = function (d) {
	// essai de variable globale pour  result0, mais c'est naze et ça ne donne rien
	var result0='rien0';
	var result2='rien2' ;

	var timeOutId;
       
        // d[2] c'est le nb de lignes attendues pré-calculées
	if(d[2]>0) {
	
	  function ajaxFn () {
	  
	  return $.ajax(mon_url, {
		  async: true,
		  data: {MonParam: d[1]},
		  error: function (XMLHttpRequest, textStatus, errorThrown) {
			alert(errorThrown);
		  },
		}).done(function(data) {
			  if (typeof data !== 'undefined') {
				  result_ajax_json= data;
				  tableau_html=result_ajax_json.toString();

				  result0 ='<b>éléments trouvée: </b> </br>'+
				  '<div class=\"TableauPlusHtml\">'+tableau_html+'</div>';
				  clearTimeout(timeOutId);

			  } else {

				 // j'ai vu ce système sur stackoverflow, et je n'ai pas de soucis a priori. 
				 timeOutId = setTimeout(ajaxFn, 10);
			  }
			  //-- 3 --- ça fonctionne jusqu'ici je récupère les bonnes valeurs quand je regarde avec F12
			  return result0;
		});

	  }
	  //-- 2 --- Je passe bien là car ajaxFn() est bien appelé, et  je vois bien dans F12 le GET avec les bonnes valeurs,
	  var promise = ajaxFn();
	  //-- 4 ---  mais je n'arrive pas à récupérer la valeur de sortie du done()
	  // J'ai même essayé avec en mettant en variable globale result0 mais c'est mal et de toute façon ça ne marche pas
	  result2= ???? fonction de promise ?;

	} else {
	  // là ça marche bien, normal je ne suis pas passé par l'ajax 
	  return result2;
	}
}