web-dev-qa-db-fra.com

L'exécution d'un fichier js renvoyé dans une réponse ajax

J'ai une application HTML5 qui utilise jquery 3.2.1.

Dans une partie de l'application - une fonction de recherche - je fais une demande ajax. La réponse de la demande ajax est HTML et elle inclut une balise <script> qui renvoie à un fichier js hébergé sur le même serveur que l'application.

Donc, le code ajax ressemble à ceci - pour faire la demande ajax et écrire la réponse à un div avec l'ID #ajaxContent:

$.ajax({
    url: $('#searchRegulations').attr('action'),
    type: 'post',
    cache: false,
    data: $('#searchRegulations').serialize()
 }).done(function (response, status, xhr) {
        if (response) {
            $('main .content').hide();
            $('#ajaxContent').html(response).show();
            return false;
        }
    }
});

Si j'inspecte #ajaxContent, je peux voir que la balise <script> est incluse dans la réponse ajax:

 enter image description here

J'ai également consulté l'onglet Réseau pour vérifier que /js/search_regulations.js est chargé correctement et donne une réponse 200:

Dans search_regulations.js, il y a une requête qui bascule certains filtres présents dans #ajaxContent

Le problème est que ce code ne semble fonctionner que pour environ 50% de le temps. Quand cela fonctionnera, il basculera l’état de certains filtres boutons en ajoutant/supprimant une classe .active aux éléments à l'intérieur de .browse-ctp__filters-data et ensuite les écrire sous une forme cachée avec l'ID #tmpFilters.

Pour être sûr que le script se "déclenche", je mets dans la ligne console.log('search_regulations.js firing'); et bien sûr, cela apparaît dans la console à chaque fois, que le script fonctionne ou non.

Ce qui est étrange, c'est que si je coupe/colle le code dans ma console après que la réponse ajax a été écrite sur la page, cela fonctionne toujours comme prévu.

Est-ce un problème avec la façon dont le script est introduit dans la page?

J'ai collé le script ci-dessous, mais je ne pense pas que le code qu'il contient pose problème, mais plutôt la manière dont la réponse navigateur/ajax est traitée:

$(function() {  

console.log('search_regulations.js firing');

/* toggle the active (applied) state on browse by filters */
/* @link https://stackoverflow.com/questions/48662677/switch-active-class-between-groups-of-include-exclude-buttons */
$(document).on('click', '.browse-ctp__filters-data .include, .exclude', function(){

    var $this = $(this);

    // Split name into array (e.g. "find_355" == ["find", "355"]) 
    var arr = $this.attr('name').split('_');


    // Toggle active class
    $this.toggleClass("active");
    if ($this.siblings().hasClass("active")) {
      $this.siblings().removeClass("active")
    }

    // Remove any existing instances of the filter from hidden form
    $('#tmpFilters input[value="exclude_'+arr[1]+'"]').remove();
    $('#tmpFilters input[value="find_'+arr[1]+'"]').remove();

    // If a filter has been applied then add it to hidden form
    if ($this.hasClass('active')) {
        $('#tmpFilters').append('<input type="hidden" name="tmpFilter[]" value="'+$this.attr('name')+'">');
    }
});    
}); 

Notes sur le Bounty offert:

J'ai offert une prime parce que ce n'est pas un problème trivial à résoudre - comme le prouve le fait que personne n'a donné de réponse valable. J'attends la bonne réponse à:

  1. Être démontrable avec jsfiddle ou équivalent.
  2. Expliquez comment/pourquoi cela fonctionne.
  3. Comprenez que la réponse ajax est HTML et js. Le js agit sur les éléments HTML de la réponse. Par conséquent, both le HTML et le JS doivent être inclus dans la réponse - au lieu de dire "ajoutez simplement le JS à un fichier global" (je ne veux pas que le JS soit global, car il est spécifique au HTML réponse donnée et peut varier selon les parties de l’application).
  4. Ne devrait pas utiliser un délai d'attente (setTimeout ou autre). Si l'utilisateur interagit avec les éléments de l'interface utilisateur - tels que les boutons - renvoyés dans la réponse HTML avant le délai d'attente et, par conséquent, js est déclenché ... cela entraîne simplement le même problème que nous avons maintenant. Donc, ce n'est pas une solution valable en ce qui me concerne.
  5. Si ce problème est impossible à résoudre dans HTML5/jQuery, expliquez pourquoi et suggérez toute autre manière de le gérer.

jsfiddle montrant les balises HTML et script renvoyées via ajax:

Plusieurs personnes ont demandé un violon ou une démo. N ° 2 personnes obtiendraient le même résultat - ce qui est bien le but de la question - donc je n’en ai pas fait un à l’origine. Le code HTML renvoyé qui est écrit dans #ajaxContent est affiché ici: http://jsfiddle.net/v4t9j32g/1/ - c'est ce que les outils de développement du navigateur affichent après la réponse ajax. Veuillez noter que la longueur du contenu renvoyé peut varier, car c'est la réponse à une fonction de recherche par mot clé qui ramène une charge de boutons de filtrage. Notez également que cette réponse HTML contient la ligne <script src="/js/search_regulations.js"></script>. C'est là que se trouve le problème js et que le contenu complet de celui-ci est présenté ci-dessus dans cette question - le bit qui contient console.log('search_regulations.js firing')

15
Andy

L'un des problèmes que je vois est que vous liez la onclick aux mêmes éléments plusieurs fois ...

Etant donné que les js peuvent être chargés plusieurs fois via des requêtes ajax, il est important de les détacher avant de rattacher des événements.

L’autre problème, c’est que vous exécutez le code dans l’événement $(document).ready (c’est-à-dire que HTML-Document est chargé et que DOM est prêt), mais il serait probablement préférable d’exécuter le code dans l’événement $(window).load peu plus tard, lorsque la page complète est entièrement chargée, y compris tous les cadres, objets et images)

Exemple :

$(window).load(function() {  

    console.log('search_regulations.js firing');

    //off: detach the events
    //on: attach the events
    $('.browse-ctp__filters-data .include, .exclude').off('click').on('click', function(){
        ...
    }
}); 
4
bastos.sergio

REMARQUE:

  • @ bastos.sergios a déjà répondu à votre question - voyez son premier point.
  • Nous devons également remercier @ yts pour "renforcer" la réponse de Bastos.

Donc, ce qui suit devrait résoudre le problème avec votre script search_regulations.js:

$(document)
  // Detach the event handler to prevent multiple/repeat triggers on the target elements.
  .off('click.toggleActive', '.browse-ctp__filters-data .include, .exclude')
  // Attach/re-attach the handler.
  .on('click.toggleActive', '.browse-ctp__filters-data .include, .exclude', function(){
    ... put your code here ...
  });

Mais mon primaire intention d'écrire cette réponse est de vous laisser voir le problème avec votre script .

Et si vous êtes curieux, voici le script que j'ai utilisé avec cette démo, qui a légèrement modifié:

https://codepen.io/anon/pen/RBjPjw.js

1
Sally CJ

En plus de ce que tout le monde parlait dans les commentaires, je vais essayer de le mettre en réponse. 

Event driven

JS dépend des événements, cela ne signifie pas que vous devez charger des scripts à la volée pour les obliger à exécuter des fonctions lorsqu'un événement est déclenché. Une meilleure approche consisterait à charger tout ce dont vous avez besoin dans le chargement de la page et à ajouter des écouteurs d’événements, ce qui offrira une bien meilleure expérience utilisateur (moins de requêtes http) et une bien meilleure codabilité.

TL; DR

En général, je ferais quelque chose comme ça dans votre fichier html:

<script src="/js/main.js">  
<script src="/js/search_regulations.js" async>  <!-- bonus tip! google "async script tag" -->

Cela chargera tous les js dont vous avez besoin. 

Gardez à l'esprit que search_regulations.js devrait ajouter l'écouteur d'événement souhaité avec la méthode jQuery on, mais comme le code html n'existait pas lorsque le script a ajouté l'écouteur d'événement, vous pouvez vérifier this .

Pourquoi c'est mieux?

  • Vous avez maintenant un cas assez facile où un fichier en charge un autre. Dans le futur, vous voudrez peut-être ajouter plus de fonctionnalités et du code plus complexe. Cela sera difficile à déboguer lorsque vous avez des chaînes de scripts qui se chargent.
  • L'utilisateur doit attendre que la demande de chargement du fichier search_regulations.js soit chargée. S'ils se trouvent sur un réseau mobile, la taille du fichier augmentera, ce qui risque de constituer une expérience utilisateur médiocre.
  • Vous pouvez toujours réutiliser votre code sur l'application avec search_regulations.js 
1
Or Duan

A propos des exigences

La solution au problème est disponible en utilisant soit HTML5/jQuery, soit une combinaison d’une version précédente de HTML et de Javascript natif. La seule raison d'utiliser une bibliothèque telle que jQuery est de mettre le même navigateur au même niveau de support, de corriger les bugs et de disposer d'un framework standard partagé entre plusieurs groupes de développeurs.

De plus, à mon avis, peu importe où vous placez les quelques lignes de code nécessaires pour résoudre le problème, elles peuvent être placées dans la section d'en-tête de la page, au bas de la page ou selon les exigences du prime dans la charge utile de la réponse Ajax qui est rechargée fréquemment.

A propos du problème

La vraie astuce réside dans la méthode que vous utilisez pour déclencher des événements de clic, votre choix de le faire dans le script présent dans les réponses Ajax répétées est ce qui fait que votre cade échoue à un moment donné.

Chaque fois qu'une requête Ajax se produit, le script de la réponse ajoute de nouveaux écouteurs d'événement. Celles-ci s'ajouteront en permanence jusqu'à ce que les navigateurs échouent pour une raison quelconque (mémoire insuffisante, pile d'événements épuisée ou conditions de compétition des événements de clic).

A propos de la solution

La solution à votre problème serait donc d'éviter d'ajouter des événements chaque fois que la demande/réponse Ajax se produit. C'est une tâche qui peut être facilement résolue par une simple "délégation d'événement de phase de capture" (malheureusement rarement utilisée).

Un écouteur d'événement est ajouté une seule fois sur l'un des noeuds supérieurs, qui peut être le noeud "document" lui-même ou l'élément "html" ou l'élément "body", donc sur un élément toujours présent dans le DOM après la page initiale. charge.

Cet élément sera en charge de capturer tout le clic sur la page, présent ou chargé ultérieurement (par vos requêtes Ajax), et cet élément agira de cette façon pendant toute la durée de vie de la page, pas besoin de configuration/Détruisez les événements comme je vois certains suggérés. Cela fonctionnera probablement aussi, mais générera plus de charge de travail sur le moteur de navigateur, consommera plus de mémoire, nécessitera plus de code et, globalement, sera plus lent et inutilement délicat.

Échantillons de code

J'ai configuré deux exemples CodePen pour vous:

Délégation1 a un code de gestion d’événement dans la section HEAD, c’est la méthode que je préfère pour traiter ce type de problèmes s’il n’existe aucune restriction ou autre problème inconnu. Les avantages de cette version sont un code plus court/plus rapide et plus facile à gérer.

Délégation1

Délégation2 a un code de gestion d'événement dans la réponse Ajax et doit respecter les exigences relatives au chargement continu de HTML/JS. Il y a une vérification pour installer seulement l'écouteur une fois et éviter les événements multiples qui se chevauchent.

Délégation2

Le code Javascript dans les exemples contient des commentaires pertinents et devrait suffire à expliquer la stratégie mise en œuvre pour rendre cela aussi simple que possible et réutilisable dans d'autres cas similaires.

Ces exemples ont été conçus pour les anciens navigateurs. Les plus récents exposent des API plus puissantes, telles que element.matches(selector), très utiles pour la délégation d’événements. J'ai délibérément évité de l'utiliser pour un soutien plus large.

1
Diego Perini

Pour éviter la possibilité d'une condition de concurrence sans implémenter de temporisateur plat, vous devez renvoyer une réponse ajax séparant le script du html, telle qu'un objet json renvoyant deux éléments (le script ou un tableau de scripts permettant de prendre en charge les cas où vous avez besoin de plusieurs éléments et d’un deuxième élément contenant le balisage HTML stringifié).

Exemple de réponse json de ajax:

{
    "script": "/yourscript.js",
    "html": "<p>Your markup...</p>"
}

Ce json (analysé) sera appelé xhrResponse ci-dessous par souci de brièveté.

Avant d’appliquer sur le dom, ajoutez une balise de précharge à votre/vos script (s) avant de charger l’élément dom, afin qu’ils commencent à être résolus en arrière-plan sans chargement, ce qui devrait minimiser le temps de chargement global et vous aider. atténuer une grande partie de la possibilité de conditions de course en cours:

document.getElementsByTagName('head')[0].appendChild( '<link rel="preload" href="' + xhrResponse.script + '" as="script">' );

Cela commencera à charger votre script en arrière-plan sans l'exécuter, afin qu'il puisse être appliqué immédiatement lorsque vous appliquerez la balise de script au dom ultérieurement.

Vous voudrez ensuite ajouter les éléments dom ensuite, puis appliquer les scripts après la résolution du nouveau contenu dom. Vous pouvez alternativement passer le balisage directement dans la réponse JSON ou alternativement renvoyer un lien vers un fichier de modèle et le servir conjointement avec la méthode de préchargement susmentionnée pour le modèle lui-même. Si vous êtes plus préoccupé par la possibilité d’éviter des problèmes de contenu, faites-le. Si vous êtes plus préoccupé par la bande passante et la latence, faites le premier. Selon votre cas d'utilisation, il existe quelques réserves mineures aux deux.

document.getElementsByTagName('head')[0].appendChild( xhrResponse.html );

Vous voudrez alors vérifier que le contenu HTML appliqué a été résolu dans le dom. Un minuteur d'attente arbitraire n'est pas un très bon moyen de le faire, car il est très probable que l'utilisateur attende plus longtemps que nécessaire, et des goulets d'étranglement occasionnels sur le réseau peuvent également provoquer l'étouffement occasionnel du script si le contenu ne se résout pas avant. la longueur arbitraire de votre minuterie (un problème plus important si vous présentez un modèle à partir d'un lien plutôt que d'un balisage HTML chiffré), ou si l'utilisateur final dispose actuellement d'une mémoire insuffisante (anciennes machines, bzillions d'onglets ouverts ou très grandes charges utiles HTML, même lorsque servi directement stratifié).

Veuillez consulter cette réponse pour une solution assez robuste permettant de vérifier si le contenu dom ajouté dynamiquement a été résolu correctement.

Une fois que cela est résolu, appliquez ensuite les éléments de script avec quelque chose comme ceci:

var script = document.createElement('script');
script.src = xhrResponse.script;
script.async = true; // use false here if you want synchronous loading for some reason
document.head.appendChild(script);

La balise de préchargement mentionnée précédemment devrait garantir que votre script a déjà été localisé par le navigateur ou qu'il est presque terminé. Vous pouvez ensuite vérifier si un écouteur d'événement d'état prêt a abouti si des problèmes subsistent:

script.onreadystatechange = function() {
    if (script.readyState === "complete") {
        // debug stuff, or call an init method for your js if you really want to be sure it fired after everything else is done.
    }
}

Il convient de noter que, dans l'écouteur d'état prêt ci-dessus, si le préchargement est résolu avant cette partie de votre logique, l'état prêt ne changera pas (car il est déjà terminé) et cette partie de la logique ne s'exécutera pas. Selon le poids de votre contenu DOM, cela peut ou non s’appliquer à vous.


Cette approche garantit le chargement dans le bon ordre, ainsi que le découplage de votre script et de vos balises, de sorte que l'un ou l'autre puisse être recyclé indépendamment l'un de l'autre, si applicable maintenant ou ultérieurement.

0
mopsyd

Vous devez supprimer la balise <script> du code HTML et créer manuellement une balise de script à ajouter au DOM. Pour l'essentiel, vous pouvez convertir le code HTML en JS via:

var $html = $(data.Html)

Ensuite, retirez toutes les balises de script comme ceci:

 var scripts = [];
        $html.each(function(i, item) {
            if (item instanceof HTMLScriptElement) {
                scripts.Push(item);
            }
        });

Ensuite, vous devez supprimer toutes les balises <script> du code HTML avant de les ajouter au DOM.

$html.find("script").remove();

$("placeToAddHtml").append($html);

Une fois que le HTML, dépouillé des balises, est ajouté, vous pouvez ensuite ajouter manuellement chaque script au DOM à la fin:

           for (var i = 0; i < scripts.length; i++) {
                var script = document.createElement('script');

                $(scripts[i]).each(function() {
                    $.each(this.attributes, function(j, attrib) {
                        var name = attrib.name;
                        var value = attrib.value;
                        script[name] = value;
                    });
                });

                script.text = scripts[i].innerHTML;
                document.body.appendChild(script);
            }

J'espère que cela fonctionne comme vous le souhaitez!

Éditer: Vous devrez peut-être créer la balise de script ici de manière à ajouter tous les scripts extraits en même temps s'ils dépendent les uns des autres. Ainsi, vous n’ajoutez qu’une seule étiquette de script géante contenant toutes les autres étiquettes en même temps.

0
Daniel Lorenz