Maison > interface Web > tutoriel CSS > Envoyer de manière fiable une demande HTTP en tant qu'utilisateur quitte une page

Envoyer de manière fiable une demande HTTP en tant qu'utilisateur quitte une page

William Shakespeare
Libérer: 2025-03-14 10:06:09
original
468 Les gens l'ont consulté

Envoyer de manière fiable une demande HTTP en tant qu'utilisateur quitte une page

Dans de nombreux cas, je dois envoyer une demande HTTP contenant des données pour enregistrer ce que fait l'utilisateur, comme la navigation vers une autre page ou la soumission d'un formulaire. Considérez cet exemple, qui envoie des informations à un service externe lorsque vous cliquez sur un lien:

 <a href="https://www.php.cn/link/3cbfb2330b21840b385a45c958602663">Aller à la page</a>

document.getElementById ('lien'). AddEventListener ('cliquez', (e) => {
  fetch ("/ log", {
    Méthode: "Post",
    En-têtes: {
      "Contenu-type": "application / json"
    }, 
    corps: json.stringify ({
      Certains: "données"
    })
  });
});
Copier après la connexion

Il n'y a rien de particulièrement compliqué ici. Le lien fonctionne comme d'habitude (je n'ai pas utilisé e.preventDefault() ), mais avant que ce comportement ne se produise, une demande post-demande est déclenchée. Pas besoin d'attendre une réponse. Je veux juste l'envoyer à n'importe quel service que je visite.

À première vue, vous pourriez penser que la planification de la demande est synchronisée, après quoi nous continuerons à naviguer à partir de la page, et d'autres serveurs traitent avec succès la demande. Mais il s'avère que ce n'est pas toujours le cas.

Le navigateur ne garantit pas que les demandes HTTP ouvertes seront conservées

Lorsque certains événements se produisent pour mettre fin à une page dans le navigateur, rien ne garantit que la demande HTTP en cours de traitement sera réussie (plus sur le cycle de vie de la page «terminaison» et d'autres états). La fiabilité de ces demandes peut dépendre d'un certain nombre de facteurs: la connexion réseau, les performances de l'application et même la configuration des services externes lui-même.

Par conséquent, l'envoi de données à ces moments peut être loin d'être fiable, et si vous comptez sur ces journaux pour prendre des décisions commerciales sensibles aux données, il peut y avoir un problème potentiellement important.

Pour illustrer ce manque de fiabilité, j'ai configuré une petite application Express avec une page utilisant le code ci-dessus. Lorsque le lien est cliqué, le navigateur se rend à /other , mais avant que cela ne se produise, une demande de poste est publiée.

Pendant que tout se passe, j'ouvre l'onglet "réseau" de mon navigateur et j'utilise la vitesse de connexion "lente". Après le chargement de la page, j'ai effacé les journaux et tout semble silencieux:

Mais une fois que vous avez cliqué sur le lien, les choses tournent mal. En cas de navigation, la demande est annulée.

Cela nous laisse presque incapables de nous assurer que les services externes peuvent réellement gérer la demande. Pour vérifier ce comportement, cela se produit également lorsque nous utilisons window.location pour naviguer par programme:

 document.getElementById ('lien'). AddEventListener ('cliquez', (e) => {
  E.PreventDefault ();

  // La demande a été mis en file d'attente mais a été annulée immédiatement après la navigation.
  fetch ("/ log", {
    Méthode: "Post",
    En-têtes: {
      "Contenu-type": "application / json"
    },
    corps: json.stringify ({
      Certains: «données»
    }),
  });

  window.location = e.target.href;
});
Copier après la connexion

Ces demandes inachevées peuvent être abandonnées, quelle que soit la façon ou le moment où la navigation se produit et lorsque la page active se termine.

Pourquoi a-t-il été annulé?

La cause profonde du problème est que par défaut, les demandes XHR (via fetch ou XMLHttpRequest ) sont asynchrones et non bloquantes. Une fois la demande en file d'attente, le travail réel de la demande est transmis à l'API au niveau du navigateur en arrière-plan.

C'est génial en termes de performances - vous ne voulez pas que la demande prenne le fil principal. Mais cela signifie également que lorsque les pages entrent dans l'état de «résiliation», ils risquent d'être abandonnés et rien ne garantit qu'un travail de fond peut être terminé. Voici le résumé par Google de cet état de cycle de vie particulier:

Une fois que la page commence à désinstaller et est éliminée de la mémoire par le navigateur, il est dans un état terminé. Dans cet état, aucune nouvelle tâche ne peut être démarrée et peut être résiliée si la tâche en cours fonctionne trop longtemps.

En bref, l'hypothèse de conception du navigateur est que lorsqu'une page est fermée, il n'est pas nécessaire de continuer à traiter les processus d'arrière-plan, il fait la queue.

Alors, quels sont nos choix?

Le moyen le plus évident d'éviter ce problème est de retarder autant que possible l'action de l'utilisateur jusqu'à ce que la demande renvoie une réponse. Dans le passé, cela a été fait à tort en utilisant les drapeaux de synchronisation pris en charge dans XMLHttpRequest . Mais l'utiliser bloque complètement le fil principal, provoquant de nombreux problèmes de performances - j'ai écrit quelques choses à ce sujet dans le passé - donc cette idée ne devrait même pas être prise en compte. En fait, il est sur le point de quitter la plate-forme (Chrome V80 l'a déjà supprimé).

Au lieu de cela, si vous allez adopter cette approche, il est préférable d'attendre que la promesse se résume à la réponse retournée. Une fois votre retour, vous pouvez effectuer en toute sécurité le comportement. En utilisant notre extrait de code précédent, cela pourrait ressembler à ceci:

 document.getElementById ('lien'). AddEventListener ('click', async (e) => {
  E.PreventDefault ();

  // attendez que la réponse revienne ...
  attendre fetch ("/ log", {
    Méthode: "Post",
    En-têtes: {
      "Contenu-type": "application / json"
    },
    corps: json.stringify ({
      Certains: «données»
    }),
  });

  //… puis naviguer pour partir.
  window.location = e.target.href;
});
Copier après la connexion

Cela peut faire le travail, mais présente des inconvénients non triviaux.

Premièrement, il affecte l'expérience utilisateur en retardant la survenue d'un comportement requis. La collecte de données d'analyse est certainement bénéfique pour l'entreprise (et pour les futurs utilisateurs), mais c'est loin d'être idéal car il fait payer les utilisateurs actuels pour la réalisation de ces avantages. Sans oublier, en tant que dépendance externe, tout problème de latence ou d'autres performances du service lui-même sera reflété à l'utilisateur. Si un délai d'expiration de votre service d'analyse entraîne le fait que le client ne termine pas une opération de grande valeur, tout le monde perdra.

Deuxièmement, cette approche n'est pas aussi fiable qu'elle le semble initialement, car un comportement de terminaison ne peut pas être retardé par programme. Par exemple, e.preventDefault() est inutile pour retarder l'utilisateur pour fermer l'onglet du navigateur. Ainsi, au mieux, il ne couvrira que la collecte de données de certaines actions des utilisateurs, mais pas assez pour lui faire pleinement confiance.

Demandez au navigateur de conserver les demandes inachevées

Heureusement, la plupart des navigateurs ont des options intégrées pour conserver les demandes HTTP inachevées sans compromettre l'expérience utilisateur.

Utilisez le logo Keepalive de Fetch's

Si le drapeau keepalive est défini sur true lors de l'utilisation fetch() , la demande correspondante reste ouverte même si la page qui a initié la demande a été résiliée. En utilisant notre exemple initial, cela fera ressembler l'implémentation à ceci:

 <a href="https://www.php.cn/link/3cbfb2330b21840b385a45c958602663">Aller à la page</a>

document.getElementById ('lien'). AddEventListener ('cliquez', (e) => {
  fetch ("/ log", {
    Méthode: "Post",
    En-têtes: {
      "Contenu-type": "application / json"
    },
    corps: json.stringify ({
      Certains: "données"
    }),
    keepalive: vrai
  });
});
Copier après la connexion

Lorsque vous cliquez sur la navigation du lien et de la page ne se produit, aucune annulation de demande ne se produit:

Au lieu de cela, nous obtenons un état (inconnu) simplement parce que la page active n'attend jamais la réception d'une réponse.

Un tel code unique est facile à corriger, surtout s'il fait partie d'une API de navigateur commune. Cependant, si vous recherchez une option plus centralisée avec une interface plus simple, il existe une autre façon de le faire avec presque le même support de navigateur.

Utilisez Navigator.SendBeacon ()

Navigator.sendBeacon() est spécifiquement utilisée pour envoyer des demandes à sens unique (balises). Une implémentation de base est la suivante, qui envoie un message avec un JSON Strimed et un Content-Type "texte / simple":

 navigator.sendbeacon ('/ log', json.stringify ({
  Certains: "données"
}));
Copier après la connexion

Mais cette API ne vous permet pas d'envoyer des en-têtes personnalisés. Donc, pour envoyer nos données comme "application / json", nous devons faire un petit ajustement et utiliser le blob:

 <a href="https://www.php.cn/link/3cbfb2330b21840b385a45c958602663">Aller à la page</a>

document.getElementById ('lien'). AddEventListener ('cliquez', (e) => {
  const blob = new Blob ([JSON.Stringify ({some: "data"})], {type: 'application / json; charset = utf-8'});
  navigator.sendBeacon ('/ log', blob);
});
Copier après la connexion

En fin de compte, nous avons obtenu le même résultat - la demande a été remplie même après la navigation de la page. Mais il y a certaines choses qui se produisent, ce qui peut le rendre meilleur que fetch() : les balises sont envoyées à faible priorité.

Pour une démonstration, voici ce qui s'affiche dans l'onglet réseau lorsque vous utilisez à la fois fetch() et sendBeacon() avec keepalive :

Par défaut, fetch() obtient une priorité "élevée", tandis que les balises (notées comme type "ping" ci-dessus) ont une priorité "la plus faible". C'est une bonne chose pour les demandes qui ne sont pas importantes pour les fonctionnalités de la page. Directement à partir de la spécification de la balise:

Cette spécification définit une interface qui […] minimise la concurrence des ressources avec d'autres opérations critiques tout en veillant à ce que ces demandes soient toujours traitées et livrées à la destination.

En d'autres termes, sendBeacon() garantit que ses demandes ne gênent pas les demandes qui sont vraiment importantes pour votre application et votre expérience utilisateur.

Mention honorifique des attributs de ping

Il convient de mentionner que de plus en plus de navigateurs prennent en charge les attributs ping . Lorsqu'il est attaché au lien, il émet une petite demande de poste:

<a href="https://www.php.cn/link/fef56cae0dfbabedeadb64bf881ab64f" ping="http://localhost:3000/log">
  Aller dans une autre page
</a>
Copier après la connexion

Ces en-têtes de demande contiendront la page où le lien est cliqué (ping-from) et la valeur HREF du lien (ping-to):

 <code>headers: { 'ping-from': 'http://localhost:3000/', 'ping-to': 'https://www.php.cn/link/fef56cae0dfbabedeadb64bf881ab64f' 'content-type': 'text/ping' // ...其他标头},</code>
Copier après la connexion

Il est techniquement similaire à l'envoi de balises, mais a des limites notables:

  1. Il est strictement limité à l'utilisation pour les liens, ce qui serait un mauvais choix si vous avez besoin de suivre les données liées à d'autres interactions telles que les clics de bouton ou les soumissions de formulaires.
  2. Le navigateur prend bien en charge, mais pas génial. Au moment de la rédaction du moment de la rédaction, Firefox ne lui permet spécifiquement pas par défaut.
  3. Vous ne pouvez pas envoyer de données personnalisées avec la demande. Comme mentionné précédemment, vous ne pouvez obtenir que quelques en-têtes Ping- * au maximum et tout autre en-têtes.

Dans l'ensemble, ping est un excellent outil si vous n'envoyez que des demandes simples et que vous ne souhaitez pas rédiger un javascript personnalisé. Cependant, si vous avez besoin d'envoyer plus de contenu, ce n'est peut-être pas la meilleure option.

Alors, lequel dois-je choisir?

Il y a certainement un compromis lors de l'envoi de la demande de dernière seconde en utilisant fetch() ou sendBeacon() avec keepalive . Pour aider à dire quelle approche est la meilleure pour différentes situations, voici certaines choses à considérer:

Vous pouvez choisir Fetch () keepalive dans les cas suivants:

  • Vous devez passer facilement les en-têtes personnalisés en utilisant les demandes.
  • Vous souhaitez émettre une demande de GET au Service, pas une demande de poste.
  • Vous soutenez les navigateurs plus anciens (tels que IE) et avez récupéré le polyfill.

Cependant, SendBeacon () pourrait être une meilleure option dans les cas suivants:

  • Vous faites des demandes de service simples qui ne nécessitent pas beaucoup de personnalisation.
  • Vous préférez une API plus concise et élégante.
  • Vous souhaitez vous assurer que votre demande ne rivalise pas avec d'autres demandes de grande priorité envoyées dans la demande.

Évitez de répéter les mêmes erreurs

Il y a une raison pour laquelle j'ai choisi de creuser profondément dans la façon dont le navigateur gère les demandes en cours lorsque la page se termine. Il n'y a pas longtemps, après avoir commencé à licencier des demandes immédiatement lorsque nous avons soumis le formulaire, notre équipe a découvert que la fréquence de types spécifiques de journaux analytiques a soudainement changé. Ce changement est soudain et significatif - en baisse d'environ 30% par rapport à ce que nous avons vu dans l'histoire.

Creuser dans la cause de ce problème et les outils disponibles pour éviter de réapparaître le problème a sauvé la journée. Donc, si quoi que ce soit, j'espère comprendre les nuances de ces défis pour aider quelqu'un à éviter une partie de la douleur que nous rencontrons. Bonne record!

Ce qui précède est le contenu détaillé de. pour plus d'informations, suivez d'autres articles connexes sur le site Web de PHP en chinois!

Déclaration de ce site Web
Le contenu de cet article est volontairement contribué par les internautes et les droits d'auteur appartiennent à l'auteur original. Ce site n'assume aucune responsabilité légale correspondante. Si vous trouvez un contenu suspecté de plagiat ou de contrefaçon, veuillez contacter admin@php.cn
Derniers articles par auteur
Tutoriels populaires
Plus>
Derniers téléchargements
Plus>
effets Web
Code source du site Web
Matériel du site Web
Modèle frontal