paint-brush
Un guide sur la façon d'annuler les demandes de récupération en double dans les formulaires améliorés JavaScriptpar@austingil
2,570 lectures
2,570 lectures

Un guide sur la façon d'annuler les demandes de récupération en double dans les formulaires améliorés JavaScript

par Austin Gil8m2023/02/10
Read on Terminal Reader

Trop long; Pour lire

Il y a de fortes chances que vous ayez accidentellement introduit un bogue de demande de duplication/condition de concurrence. Aujourd'hui, je vais vous expliquer le problème et mes recommandations pour l'éviter. Le problème clé ici est le`event.preventDefault()`. Cette méthode empêche le navigateur d'exécuter le comportement par défaut de chargement de la nouvelle page et de soumission du formulaire.
featured image - Un guide sur la façon d'annuler les demandes de récupération en double dans les formulaires améliorés JavaScript
Austin Gil HackerNoon profile picture

Si vous avez déjà utilisé l'API fetch JavaScript pour améliorer la soumission d'un formulaire, il y a de fortes chances que vous ayez accidentellement introduit un bogue de demande en double/condition de concurrence. Aujourd'hui, je vais vous expliquer le problème et mes recommandations pour l'éviter.


(Vidéo à la fin si vous préférez)


Considérons un très basique Formulaire HTML avec une seule entrée et un bouton d'envoi.


 <form method="post"> <label for="name">Name</label> <input id="name" name="name" /> <button>Submit</button> </form> 


Lorsque nous appuyons sur le bouton Soumettre, le navigateur effectue une actualisation complète de la page.


Remarquez comment le navigateur se recharge après avoir cliqué sur le bouton Soumettre.


L'actualisation de la page n'est pas toujours l'expérience que nous voulons offrir à nos utilisateurs, donc une alternative courante consiste à utiliser Javascript pour ajouter un écouteur d'événement à l'événement « soumettre » du formulaire, empêcher le comportement par défaut et soumettre les données du formulaire à l'aide de l'API fetch .


Une approche simpliste pourrait ressembler à l'exemple ci-dessous.


Après le montage de la page (ou du composant), nous récupérons le nœud DOM du formulaire, ajoutons un écouteur d'événement qui construit une requête fetch à l'aide du formulaire action , méthode , et données , et à la fin du gestionnaire, nous appelons la méthode preventDefault() de l'événement.


 const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); function handleSubmit(event) { const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }); event.preventDefault(); }


Maintenant, avant que les hotshots JavaScript ne commencent à me tweeter à propos de GET vs POST et du corps de la demande et Type de contenu et quoi que ce soit d'autre, permettez-moi de dire, je sais. Je garde la demande fetch délibérément simple car ce n'est pas l'objectif principal.


Le problème clé ici est le event.preventDefault() . Cette méthode empêche le navigateur d'exécuter le comportement par défaut de chargement de la nouvelle page et de soumission du formulaire.


Maintenant, si nous regardons l'écran et que nous appuyons sur soumettre, nous pouvons voir que la page ne se recharge pas, mais nous voyons la requête HTTP dans notre onglet réseau.


Notez que le navigateur n'effectue pas de rechargement complet de la page.


Malheureusement, en utilisant JavaScript pour empêcher le comportement par défaut, nous avons en fait introduit un bogue que le comportement par défaut du navigateur n'a pas.


Lorsque nous utilisons plaine HTML et que vous écrasez le bouton d'envoi plusieurs fois très rapidement, vous remarquerez que toutes les requêtes réseau, à l'exception de la plus récente, deviennent rouges. Cela indique qu'ils ont été annulés et que seule la demande la plus récente est honorée.


Si nous comparons cela à l'exemple JavaScript, nous verrons que toutes les requêtes sont envoyées, et toutes sont complètes sans qu'aucune ne soit annulée.


Cela peut être un problème, car même si chaque demande peut prendre un temps différent, elles peuvent être résolues dans un ordre différent de celui dans lequel elles ont été lancées. Cela signifie que si nous ajoutons des fonctionnalités à la résolution de ces demandes, nous pourrions avoir un comportement inattendu.


A titre d'exemple, nous pourrions créer une variable à incrémenter pour chaque requête (« totalRequestCount »). Chaque fois que nous exécutons la fonction handleSubmit , nous pouvons incrémenter le nombre total ainsi que capturer le nombre actuel pour suivre la demande actuelle (« thisRequestNumber »).


Lorsqu'une requête fetch est résolue, nous pouvons enregistrer son numéro correspondant dans la console.


 const form = document.querySelector('form'); form.addEventListener('submit', handleSubmit); let totalRequestCount = 0 function handleSubmit(event) { totalRequestCount += 1 const thisRequestNumber = totalRequestCount const form = event.currentTarget; fetch(form.action, { method: form.method, body: new FormData(form) }).then(() => { console.log(thisRequestNumber) }) event.preventDefault(); }


Maintenant, si nous frappons ce bouton de soumission plusieurs fois, nous pourrions voir différents numéros imprimés sur la console dans le désordre : 2, 3, 1, 4, 5. Cela dépend de la vitesse du réseau, mais je pense que nous pouvons tous être d'accord que ce n'est pas idéal.


Considérez un scénario dans lequel un utilisateur déclenche plusieurs requêtes fetch en succession rapprochée et, une fois terminées, votre application met à jour la page avec ses modifications. L'utilisateur pourrait finalement voir des informations inexactes en raison de demandes résolues dans le désordre.


Il ne s'agit pas d'un problème dans le monde non-JavaScript, car le navigateur annule toute requête précédente et charge la page une fois la requête la plus récente terminée, en chargeant la version la plus récente. Mais les rafraîchissements de page ne sont pas aussi sexy.


La bonne nouvelle pour les amateurs de JavaScript est que nous pouvons avoir à la fois un expérience utilisateur sexy ET une interface utilisateur cohérente !


Nous avons juste besoin de faire un peu plus de démarches.


Si vous consultez la documentation de l'API fetch , vous verrez qu'il est possible d'abandonner une récupération à l'aide d'un AbortController et de la propriété signal des options fetch . Cela ressemble à ceci :


 const controller = new AbortController(); fetch(url, { signal: controller.signal });


En fournissant le signal de AbortContoller à la requête fetch , nous pouvons annuler la requête à tout moment lorsque la méthode abort de AbortContoller est déclenchée.


Vous pouvez voir un exemple plus clair dans la console JavaScript. Essayez de créer un AbortController , en lançant la requête fetch , puis en exécutant immédiatement la méthode abort .


 const controller = new AbortController(); fetch('', { signal: controller.signal }); controller.abort()


Vous devriez immédiatement voir une exception imprimée sur la console. Dans les navigateurs Chromium, il devrait dire, "Uncaught (in promise) DOMException: L'utilisateur a abandonné une demande." Et si vous explorez l'onglet Réseau, vous devriez voir une demande ayant échoué avec le texte d'état "(annulé)".


Les outils de développement Chrome se sont ouverts sur le réseau avec la console JavaScript ouverte. Dans la console se trouve le code "const controller = new AbortController();fetch('', { signal: controller.signal });controller.abort()", suivi de l'exception, "Uncaught (in promise) DOMException : The l'utilisateur a abandonné une demande." Dans le réseau, il y a une demande à "localhost" avec le texte d'état "(annulé)"

Dans cet esprit, nous pouvons ajouter un AbortController au gestionnaire de soumission de notre formulaire. La logique sera la suivante :


  • Tout d'abord, recherchez un AbortController pour toutes les demandes précédentes. S'il en existe un, annulez-le.


  • Ensuite, créez un AbortController pour la requête en cours qui peut être abandonné lors des requêtes suivantes.


  • Enfin, lorsqu'une requête est résolue, supprimez son AbortController correspondant.


Il existe plusieurs façons de procéder, mais j'utiliserai un WeakMap pour stocker les relations entre chaque nœud DOM <form> soumis et son AbortController respectif. Lorsqu'un formulaire est soumis, nous pouvons vérifier et mettre à jour la WeakMap en conséquence.


 const pendingForms = new WeakMap(); function handleSubmit(event) { const form = event.currentTarget; const previousController = pendingForms.get(form); if (previousController) { previousController.abort(); } const controller = new AbortController(); pendingForms.set(form, controller); fetch(form.action, { method: form.method, body: new FormData(form), signal: controller.signal, }).then(() => { pendingForms.delete(form); }); event.preventDefault(); } const forms = document.querySelectorAll('form'); for (const form of forms) { form.addEventListener('submit', handleSubmit); }


L'essentiel est de pouvoir associer un contrôleur d'abandon à son formulaire correspondant. L'utilisation du nœud DOM du formulaire comme clé de WeakMap est un moyen pratique de le faire.


Avec cela en place, nous pouvons ajouter le signal de AbortController à la requête fetch , abandonner tous les contrôleurs précédents, en ajouter de nouveaux et les supprimer à la fin.


J'espère que tout cela a du sens.


Maintenant, si nous frappons plusieurs fois le bouton d'envoi de ce formulaire, nous pouvons voir que toutes les demandes d'API, à l'exception de la plus récente, sont annulées.


Cela signifie que toute fonction répondant à cette réponse HTTP se comportera davantage comme prévu.


Maintenant, si nous utilisons la même logique de comptage et de journalisation que nous avons ci-dessus, nous pouvons écraser le bouton d'envoi sept fois et verrions six exceptions (dues à AbortController ) et un journal de "7" dans la console.


Si nous soumettons à nouveau et laissons suffisamment de temps pour que la demande soit résolue, nous verrons "8" dans la console. Et si nous écrasons le bouton Soumettre un tas de fois, encore une fois, nous continuerons à voir les exceptions et le nombre de requêtes finales dans le bon ordre.


Si vous souhaitez ajouter un peu plus de logique pour éviter de voir DOMExceptions dans la console lorsqu'une requête est abandonnée, vous pouvez ajouter un bloc .catch() après votre requête fetch et vérifier si le nom de l'erreur correspond à " AbortError " :


 fetch(url, { signal: controller.signal, }).catch((error) => { // If the request was aborted, do nothing if (error.name === 'AbortError') return; // Otherwise, handle the error here or throw it back to the console throw error });

Fermeture

Tout cet article était axé sur les formulaires améliorés par JavaScript, mais c'est probablement une bonne idée d'inclure un AbortController chaque fois que vous créez une demande fetch . C'est vraiment dommage qu'il ne soit pas déjà intégré à l'API. Mais j'espère que cela vous montre une bonne méthode pour l'inclure.


Il convient également de mentionner que cette approche n'empêche pas l'utilisateur de spammer le bouton d'envoi plusieurs fois. Le bouton est toujours cliquable et la demande se déclenche toujours, cela fournit simplement un moyen plus cohérent de traiter les réponses.


Malheureusement, si un utilisateur spamme un bouton d'envoi, ces demandes iront toujours à votre backend et pourraient utiliser un tas de ressources inutiles.


Certaines solutions naïves peuvent désactiver le bouton d'envoi, en utilisant un anti-rebond , ou créer de nouvelles requêtes uniquement après la résolution des précédentes. Je n'aime pas ces options car elles reposent sur le ralentissement de l'expérience de l'utilisateur et ne fonctionnent que du côté client.


Ils ne traitent pas les abus via des requêtes scriptées.


Pour traiter les abus d'un trop grand nombre de requêtes adressées à votre serveur, vous souhaiterez probablement configurer certains limitation de débit . Cela dépasse le cadre de cet article, mais cela valait la peine d'être mentionné.


Il convient également de mentionner que la limitation du débit ne résout pas le problème initial des demandes en double, des conditions de concurrence et des mises à jour incohérentes de l'interface utilisateur. Idéalement, nous devrions utiliser les deux pour couvrir les deux extrémités.


Quoi qu'il en soit, c'est tout ce que j'ai pour aujourd'hui. Si vous voulez regarder une vidéo qui traite du même sujet, regardez ceci.

Merci beaucoup d'avoir lu. Si vous avez aimé cet article, n'hésitez pas Partagez-le . C'est l'un des meilleurs moyens de me soutenir. Vous pouvez aussi Inscrivez-vous à ma newsletter ou Suis moi sur Twitter si vous voulez savoir quand de nouveaux articles sont publiés.


Initialement publié le austingil.com .