Si vous avez déjà utilisé l'API 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. fetch (Vidéo à la fin si vous préférez) Considérons un très basique avec une seule entrée et un bouton d'envoi. Formulaire HTML <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 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 . Javascript 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 à l'aide du formulaire , , et , et à la fin du gestionnaire, nous appelons la méthode de l'événement. fetch action méthode données preventDefault() 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 et quoi que ce soit d'autre, permettez-moi de dire, je sais. Je garde la demande délibérément simple car ce n'est pas l'objectif principal. Type de contenu fetch Le problème clé ici est le . 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. event.preventDefault() 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 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. HTML 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 (« »). Chaque fois que nous exécutons la fonction , nous pouvons incrémenter le nombre total ainsi que capturer le nombre actuel pour suivre la demande actuelle (« »). totalRequestCount handleSubmit thisRequestNumber Lorsqu'une requête est résolue, nous pouvons enregistrer son numéro correspondant dans la console. fetch 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 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. fetch 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 ET une interface utilisateur cohérente ! expérience utilisateur sexy Nous avons juste besoin de faire un peu plus de démarches. Si vous consultez la documentation de l'API , vous verrez qu'il est possible d'abandonner une récupération à l'aide d'un et de la propriété des options . Cela ressemble à ceci : fetch AbortController signal fetch const controller = new AbortController(); fetch(url, { signal: controller.signal }); En fournissant le signal de à la requête , nous pouvons annuler la requête à tout moment lorsque la méthode de est déclenchée. AbortContoller fetch abort AbortContoller Vous pouvez voir un exemple plus clair dans la console JavaScript. Essayez de créer un , en lançant la requête , puis en exécutant immédiatement la méthode . AbortController fetch 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é)". Dans cet esprit, nous pouvons ajouter un au gestionnaire de soumission de notre formulaire. La logique sera la suivante : AbortController Tout d'abord, recherchez un pour toutes les demandes précédentes. S'il en existe un, annulez-le. AbortController Ensuite, créez un pour la requête en cours qui peut être abandonné lors des requêtes suivantes. AbortController Enfin, lorsqu'une requête est résolue, supprimez son correspondant. AbortController Il existe plusieurs façons de procéder, mais j'utiliserai un pour stocker les relations entre chaque nœud DOM soumis et son respectif. Lorsqu'un formulaire est soumis, nous pouvons vérifier et mettre à jour la en conséquence. WeakMap <form> AbortController WeakMap 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 est un moyen pratique de le faire. WeakMap Avec cela en place, nous pouvons ajouter le signal de à la requête , abandonner tous les contrôleurs précédents, en ajouter de nouveaux et les supprimer à la fin. AbortController fetch 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 à ) et un journal de "7" dans la console. AbortController 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 après votre requête et vérifier si le nom de l'erreur correspond à " " : .catch() fetch 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 chaque fois que vous créez une demande . 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. AbortController fetch 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 un bouton d'envoi, ces demandes iront toujours à votre backend et pourraient utiliser un tas de ressources inutiles. spamme Certaines solutions naïves peuvent désactiver le bouton d'envoi, en utilisant un , 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. anti-rebond 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 . Cela dépasse le cadre de cet article, mais cela valait la peine d'être mentionné. limitation de débit 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. https://www.youtube.com/watch?v=w8ZIoLnh1Dc&embedable=true Merci beaucoup d'avoir lu. Si vous avez aimé cet article, n'hésitez pas . C'est l'un des meilleurs moyens de me soutenir. Vous pouvez aussi ou si vous voulez savoir quand de nouveaux articles sont publiés. Partagez-le Inscrivez-vous à ma newsletter Suis moi sur Twitter Initialement publié le austingil.com .