Выполнение файла js, возвращаемого внутри ответа ajax

У меня есть приложение HTML5, которое использует jquery 3.2.1.

В части приложения - функция поиска - я делаю запрос ajax. Ответ от запроса ajax - это HTML, и это включает <script> который ссылается на файл js, который размещен на том же сервере, что и приложение.

Таким образом, код ajax выглядит так: для создания запроса ajax и записи ответа на div с идентификатором #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;
        }
    }
});

Если я проверяю #ajaxContent я вижу, что <script> включен в ответ ajax:

enter image description here

Я также проверил вкладку "Моя сеть", чтобы убедиться, что /js/search_regulations.js загружается правильно, и это дает ответ 200:

Внутри search_regulations.js есть некоторый jquery, который переключает некоторые фильтры, которые присутствуют в #ajaxContent.

Проблема в том, что этот код работает только около 50% времени. Когда он будет работать, он переключит состояние некоторых кнопок фильтра, добавив/удалив класс. .active для элементов внутри .browse-ctp__filters-data а затем записывает их в скрытую форму с идентификатором #tmpFilters.

Чтобы убедиться, что сценарий "стрелял", я ввел в строку console.log('search_regulations.js firing'); и, конечно же, это отображается в консоли каждый раз независимо от того, функционирует ли скрипт или нет.

Что странно, если я вырезал/вставлял код в свою консоль после того, как на страницу была написана ответ ajax, она всегда работает так, как ожидалось.

Это какая-то проблема с тем, как сценарий вводится на страницу?

Я вставил скрипт ниже, но я не думаю, что это проблема с кодом в нем, скорее, как обрабатывается ответ browser/ajax:

$(function() {  

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

/* toggle the active (applied) state on browse by filters */
/* @link https://stackoverflow.com/info/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')+'">');
    }
});    
}); 

Заметки о Bounty:

Я предложил щедрость, потому что это не тривиальная проблема, которую нужно решить, - продемонстрировал тот факт, что никто не дал обоснованного ответа. Я ожидаю правильного ответа на:

  1. Будьте очевидны с помощью jsfiddle или эквивалента.
  2. Объясните, как это работает.
  3. Поймите, что ответ ajax - это HTML и js. Js действует на элементы HTML в ответе. Поэтому как HTML, так и js необходимо включить в ответ - в отличие от высказывания "просто добавьте js в глобальный файл" (я не хочу, чтобы js был глобальным, потому что он специфичен для ответа HTML-ответа и могут различаться в разных частях приложения).
  4. Не следует использовать таймаут (setTimeout или что-то еще). Если пользователь взаимодействует с элементами пользовательского интерфейса - например, кнопками - возвращается в ответе HTML до истечения таймаута и, следовательно, js запускается..., что просто приводит к той же проблеме, что и сейчас. Так что это не приемлемое решение, насколько мне известно.
  5. Если эту проблему невозможно решить в HTML5/jquery, объясните, почему и предлагайте любые альтернативные способы ее устранения.

jsfiddle, показывающий тег HTML и скрипта, возвращаемый через ajax:

Несколько человек попросили скрипку или демонстрацию. Нет двух человек, получавших одинаковый результат - и это очень важно, поэтому я не делал этого изначально. #ajaxContent HTML, который записывается в #ajaxContent, показан здесь: http://jsfiddle.net/v4t9j32g/1/ - это то, что dev-инструменты в браузере показывают после ответа ajax. Обратите внимание, что длина возвращаемого содержимого может меняться, поскольку это ответ на средство поиска по ключевым словам, которое возвращает загрузку кнопок фильтра. Также обратите внимание, что этот ответ HTML содержит строку <script src="/js/search_regulations.js"></script>. Именно там находится проблематичное js, и полное содержание этого выше показано в этом вопросе - бит, который включает console.log('search_regulations.js firing')

Ответы

Ответ 1

Одна из проблем, которые я вижу, это то, что вы привязываете onclick к тем же элементам несколько раз...

Так как js можно загружать многократно через ajax-запросы, важно сначала отсоединить их, прежде чем снова присоединить события.

Другая проблема заключается в том, что вы запускаете код в событии $(document).ready (то есть когда загружается HTML-документ и DOM готов), однако вам, вероятно, будет лучше запустить код в $(window).load (который выполняет бит последний, когда полная страница полностью загружена, включая все кадры, объекты и изображения)

Пример:

$(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(){
        ...
    }
}); 

Ответ 2

В дополнение к тому, что все говорили в комментариях, я попытаюсь ответить на этот вопрос.

Событие

JS управляется событиями, это не означает, что вам приходится загружать скрипты "на лету", чтобы заставить их выполнять функции при запуске события. Лучшим подходом было бы загрузить все, что вам нужно при загрузке страницы, и добавить прослушиватели событий, это приведет к значительному улучшению работы пользователя (меньше HTTP-запросов) и значительно улучшит пригодность кода.

TL; DR

В общем, я бы сделал что-то подобное в вашем html файле:

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

Это загрузит все необходимые вам js.

Имейте в виду, что search_regulations.js должен добавить прослушиватель событий, который вы хотите, с помощью jQuery on методу, но поскольку html не существовал, когда скрипт добавил слушателя событий, вы можете проверить это.

Почему это лучше?

  • Теперь у вас был довольно простой случай, когда один файл загружает другой. В будущем вам может понадобиться добавить дополнительные функции и более сложный код. Это будет болезненно для отладки, когда у вас есть цепочки сценариев, которые загружают друг друга.
  • Пользователь должен дождаться search_regulations.js файла search_regulations.js, если он находится в мобильной сети/файл будет больше по размеру, что может быть плохой пользовательский интерфейс.
  • Вы можете повторно использовать свой код над приложением с помощью search_regulations.js

Ответ 3

НОТА:

  • @bastos.sergios уже ответил на ваш вопрос - см. его первый пункт.
  • Кредит также должен быть предоставлен @yts для "усиления" ответа Бастоса.

Таким образом, следующее должно устранить проблему с вашим 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 ...
  });

Но мое основное намерение написать этот ответ - позволить вам увидеть проблему с вашим скриптом.

И если вам интересно, вот сценарий, который я использовал с этой демонстрацией, которая слегка изменилась:

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

Ответ 4

О требованиях

Решение проблемы доступно с использованием HTML5/jQuery или комбинации предыдущей версии HTML и собственного Javascript. Единственная причина использования библиотеки, такой как jQuery, заключается в том, чтобы принести более старый браузер на тот же уровень поддержки или исправить ошибки и иметь стандартную структуру, разделяемую между группами разработчиков.

Кроме того, на мой взгляд, неважно, где вы размещаете несколько строк кода, необходимых для решения проблемы, их можно поместить в главный раздел страницы внизу страницы или в соответствии с требованиями щедрость в полезной нагрузке ответа Ajax, которая часто перезагружается.

О проблеме

Реальная трюк заключается в методе, который вы используете для достижения стрельбы по щелчкам, ваш выбор делать это внутри скрипта, присутствующего в повторных ответах Ajax, - это то, что заставляет ваш кадр терпеть неудачу в какой-то момент.

Каждый раз, когда выполняется запрос Ajax, скрипт в ответе добавляет новых прослушивателей событий. Они будут постоянно суммироваться до тех пор, пока браузеры не по каким-либо причинам не сработают (недостаточно памяти, исчерпаны события или условия соревнований событий событий).

О решении

Таким образом, решение вашей проблемы будет заключаться в том, чтобы не добавлять события каждый раз, когда возникает запрос/ответ Ajax. Это задача, которую легко решить простым "захватом фазового события" (часто не используется).

Слушатель событий добавляется только один раз на один из верхних узлов, который может быть самим "документом" или элементом "html" или "body", поэтому на одном элементе, который всегда присутствует в DOM после начальной страницы нагрузки.

Этот элемент будет отвечать за захват всего щелчка на странице, который в настоящее время присутствует или загружен позднее (по вашим запросам Ajax), и этот элемент будет действовать таким образом на весь срок службы страницы, нет необходимости в настройке/уничтожить события, как я вижу, некоторые из них. Хорошо, что, вероятно, будет работать, но будет генерировать больше рабочей нагрузки на движок браузера, потреблять больше памяти, требовать больше кода и в целом быть медленнее и излишне хитроумным, если не сказать больше.

Образцы кода

У меня есть два примера CodePen для вас:

У делегирования1 есть код обработки событий в разделе HEAD, это мой предпочтительный способ справиться с такими проблемами, если никаких ограничений или других неизвестных причуд не существует. Преимущества этой версии короче/быстрее и более удобный код.

Delegation1

Delegation2 имеет код обработки событий в ответе Ajax и должен удовлетворять требованиям, предъявляемым к загрузке HTML/JS непрерывно. Существует проверка, чтобы установить только слушателя один раз и избежать многократных гонок и перекрытия событий.

Delegation2

Код Javascript в примерах содержит соответствующие комментарии и должен быть достаточным для объяснения реализованной стратегии, чтобы сделать это максимально простым и многоразовым в других подобных случаях.

Эти примеры были написаны с использованием старых браузеров, но более новый браузер предоставляет более мощные API, такие как element.matches(selector) очень полезный при делегировании событий. Я намеренно избегал использовать его для более широкой поддержки.

Ответ 5

Вам нужно удалить тег <script> из HTML и создать тег сценария вручную, чтобы добавить в DOM. По сути, вы можете конвертировать Html в JS через:

var $html = $(data.Html)

Затем вытащите все теги скрипта, как это:

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

Затем вам нужно удалить все теги <script> из html перед добавлением его в DOM.

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

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

Как только HTML, лишенный тегов, добавлен, вы можете вручную добавить каждый скрипт в DOM в конце:

           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);
            }

Надеюсь, это сработает, как вы это намереваетесь!

Изменить. Возможно, вам нужно будет создать тег сценария таким образом, чтобы одновременно добавлять все вытащенные скрипты, если они зависят друг от друга. Таким образом, вы добавляете только один гигантский тег скрипта, который одновременно имеет все остальные теги.

Ответ 6

Чтобы избежать возможности гонки, не применяя плоский таймер, вам нужно будет вернуть ответ ajax, который отделяет скрипт от html, например json-объект, который возвращает два элемента (сценарий или массив скриптов для поддержки случаев где вам нужно несколько, и второй элемент, содержащий строчную разметку html).

Пример ответа json от ajax:

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

Этот (разобранный) json будет обозначаться как xhrResponse ниже для краткости.

Перед тем, как применить к dom, добавьте тег предварительной загрузки для своего сценария (ов) до загрузки элемента dom, чтобы они начали разрешаться в фоновом режиме без загрузки, что должно минимизировать общую временную шкалу загрузки и помочь облегчить большую часть возможность постоянных условий гонки:

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

Это начнет загружать ваш сценарий в фоновом режиме, не выполняя его, поэтому его можно применить сразу же после применения тега сценария к dom позже.

Затем вы захотите добавить элементы dom далее, а затем применить скрипты после того, как новый контент dom будет разрешен. Вы можете поочередно передавать разметку непосредственно в ответ json или поочередно возвращать ссылку на файл шаблона и обслуживать это в сочетании с вышеупомянутым методом предварительной загрузки для самого шаблона. Если вас больше интересует возможность избежать проблем с контентом, сделайте последнее. Если вы больше озабочены пропускной способностью и задержкой, сделайте первый. Есть некоторые незначительные оговорки для обоих, в зависимости от вашего варианта использования.

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

Затем вы захотите проверить, разрешено ли примененное содержимое html внутри dom. Произвольный таймер ожидания не очень хороший способ сделать это, поскольку он имеет высокую вероятность заставить пользователя ждать дольше, чем это необходимо, и иногда узкие места в сети могут также приводить к тому, что ваш скрипт иногда задыхается, если контент не разрешается до произвольную длину таймера (большая часть проблемы, если вы используете шаблон из ссылки вместо строковой разметки html), или если у конечного пользователя имеется доступная в настоящий момент память (старые машины, открытые bzillion вкладки или очень большие html файлы, даже если служил непосредственно стянутым).

Пожалуйста, см. Этот ответ для довольно надежного решения, чтобы проверить, правильно ли разрешен динамически добавленный контент dom.

После того, как это будет разрешено, затем примените элементы сценария после этого примерно так:

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);

Ранее упомянутый тег preload должен гарантировать, что ваш скрипт уже был локализован браузером к этому моменту или почти так же сделал это. Затем вы можете проверить, завершено ли оно с помощью готового слушателя событий состояния, если он все еще дает вам проблемы:

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.
    }
}

Следует отметить, что в вышеприведенном слушателе состояния, если предварительная нагрузка разрешается до этой части вашей логики, состояние готовности не изменится (поскольку оно уже выполнено), и эта часть логики не будет выполняться. В зависимости от веса вашего содержимого DOM это может или не может быть применимо к вам.


Такой подход обеспечит загрузку в правильном порядке, а также отделит ваш скрипт и разметку, поэтому либо можно перерабатывать независимо друг от друга в другом месте, если это применимо сейчас или в любой момент в будущем.

Ответ 7

Думал, что если вы добавите это в свой параметр ajax params, все будет в порядке:

async: false

Ответ 8

Я думаю, что проблемы могут быть следующими: 1. Включить и исключить триггеры той же функции. Ошибка в логике внутри для обработки обоих. (так как вы сказали, что он работает в 50% случаев). 2. Попробуйте обновить DOM после добавления HTML.

   componentHandler.upgradeDom();

сразу после

 $('#ajaxContent').html(response).show();

Надеюсь, поможет !!!