【问题标题】:Executing a js file returned inside an ajax response执行在 ajax 响应中返回的 js 文件
【发布时间】:2018-12-28 16:52:36
【问题描述】:

我有一个使用 jquery 3.2.1 的 HTML5 应用程序。

在应用程序的一部分 - 一个搜索功能 - 我提出了一个 ajax 请求。 ajax 请求的响应是 HTML,其中包括一个 <script> 标记,该标记链接到与应用程序位于同一服务器上的 js 文件。

因此 ajax 代码如下所示 - 用于发出 ajax 请求并将响应写入 ID 为 #ajaxContent 的 div:

$.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 响应中:

我还检查了我的网络选项卡,以确保 /js/search_regulations.js 被正确加载,并且它给出了 200 响应:

search_regulations.js 内部有一些jquery 可以切换#ajaxContent 中存在的一些过滤器。

问题是这段代码似乎只工作了大约 50% 时间。当它工作时,它将切换某些过滤器的状态 通过向内部元素添加/删除类.active 按钮 .browse-ctp__filters-data 然后将它们写入隐藏表单 ID 为#tmpFilters

为了确保脚本“触发”,我输入了console.log('search_regulations.js firing'); 行,果然,这每次都会显示在控制台中,无论脚本是否正常运行。

奇怪的是,如果我在将 ajax 响应写入页面后将代码剪切/粘贴到控制台中,它总是可以正常工作

这是将脚本带入页面的方式有问题吗?

我已经粘贴了下面的脚本,但我认为这不是其中代码的问题,而是浏览器/ajax 响应的处理方式:

$(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')+'">');
    }
});    
}); 

关于提供的赏金的说明:

我提供了赏金,因为这不是一个需要解决的小问题 - 没有人给出可行的答案这一事实证明了这一点。我希望得到正确答案:

  1. 可使用 jsfiddle 或同等工具进行演示。
  2. 解释其工作原理/原因。
  3. 了解ajax响应是HTML和js。 js 作用于响应中的 HTML 元素。因此 both HTML 和 js 需要包含在响应中 - 而不是说“只需将 js 添加到全局文件”(我不希望 js 是全局的,因为它是特定的到给定的 HTML 响应,并且可以在应用程序的不同部分有所不同)。
  4. 不应使用超时(setTimeout 或其他)。如果用户与 UI 元素交互 - 例如按钮 - 在 HTML 响应中返回 before 超时,因此 js 被触发......这只会导致我们现在遇到的同样问题。因此,就我而言,这不是一个有效的解决方案。
  5. 如果此问题在 HTML5/jquery 中无法解决,请解释原因并提出其他处理方法。

jsfiddle 显示通过 ajax 返回的 HTML 和 script 标签:

有几个人要求提供小提琴或演示。没有两个人会得到相同的结果——这正是问题的重点——所以我最初没有做一个。返回的写入#ajaxContent 的 HTML 如下所示:http://jsfiddle.net/v4t9j32g/1/ - 这是浏览器中的开发工具在 ajax 响应之后显示的内容。请注意,返回内容的长度可能会有所不同,因为它是对关键字搜索工具的响应,会带回大量过滤器按钮。另请注意,此 HTML 响应包含行 &lt;script src="/js/search_regulations.js"&gt;&lt;/script&gt;这就是有问题的 js 所在的位置,并且上面显示了这个问题的全部内容 - 包含 console.log('search_regulations.js firing') 的位

【问题讨论】:

  • 尝试加入 'defer' - &lt;script defer src=....
  • 正如 wazz 所说,这很可能是您的新元素尚未安装到 DOM 的竞争条件。在该脚本中记录选择器的结果,当它不起作用时,您可能会看到找不到它们。 PS 返回 HTML 甚至更糟糕的是 ajax 响应中的脚本非常老派并且不受欢迎,最好返回原始 JSON 数据并按照您的喜好呈现。
  • @DominicTobias 谢谢。我提供赏金的原因是因为我需要一个完整的示例来详细解释所有这些概念。特别是返回 JSON 并呈现它......这如何帮助克服 &lt;script&gt; 标签的问题 - 即使必须将响应从 JSON 转换为 HTML,仍然需要 js,所以不确定这有什么帮助?跨度>
  • 有什么方法可以创建一个发生这种情况的工作示例? @安迪
  • 您可以通过将 search_regulations.js 所做的更改包装在 SetTimeout 方法中来证明它是否是竞争条件。将其设置为延迟 5000 毫秒。如果这使事情每次都能完美运行。然后你肯定知道这是一个竞争条件。

标签: javascript jquery html ajax


【解决方案1】:

我看到的一个问题是您将onclick 绑定到相同的元素 多次...

由于 js 可以通过 ajax 请求多次加载,因此在再次附加事件之前先分离很重要。

另一个问题是,您在 $(document).ready 事件中运行代码(即当 HTML 文档已加载且 DOM 准备就绪时),但是您最好在 @ 中运行代码987654323@事件(稍后执行,当整个页面完全加载时,包括所有框架、对象和图像)

示例

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

【讨论】:

  • 我同意你的第一点。如果代码加载了偶数次,toggle 将自行反转,看起来就像什么也没发生。 .load 部分无关紧要,因为所有这些都是在 ajax 调用之后完成的,并且元素是否存在并不重要(事件处理程序附加到文档,而不是 dom 元素)。但是有一个修改:不是调用.off('click')(这可能会删除其他点击处理程序),而是应该给事件一个命名空间,以便保存删除。 ).off('click.toggleActive').on('click.toggleActive', ...)
【解决方案2】:

除了每个人在 cmets 中都在谈论的内容之外,我将尝试将其放入答案中。

事件驱动

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 不存在,您可能需要检查 this

为什么这样更好?

  • 现在您遇到了一个非常简单的情况,即一个文件加载另一个文件。将来您可能希望添加更多功能和更复杂的代码。当您有相互加载的脚本链时,调试起来会很痛苦。
  • 如果用户在移动网络上/文件会变大,可能会影响用户体验,用户必须等到加载 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 ...
      });
    

    但是我写这个答案的主要意图是让你see the problem with your script

    如果你好奇,这里是我在那个演示中使用的脚本,稍作修改:

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

    【讨论】:

      【解决方案4】:

      关于要求

      可以使用 HTML5/jQuery 或以前的 HTML 版本和本机 Javascript 的组合来解决该问题。使用像 jQuery 这样的库的唯一原因是让旧浏览器获得相同级别的支持或修复错误并在开发人员组之间共享标准框架。

      另外,在我看来,解决问题所需的几行代码放在哪里并不重要,它们可以放在页面的头部、页面底部或频繁重新加载的 Ajax 响应负载中的赏金需求。

      关于问题

      真正的诀窍在于您用于触发点击事件的方法,您选择在重复的 Ajax 响应中出现的脚本中执行此操作是导致您的 cade 在某些时候失败的原因。

      每次发生 Ajax 请求时,响应中的脚本都会添加新的事件侦听器。这些将不断累加,直到浏览器由于某种原因(内存不足、事件堆栈耗尽或点击事件竞争条件)失败。

      关于解决方案

      因此,您的问题的解决方案是避免在每次 Ajax 请求/响应发生时添加事件。这是一项可以通过简单的“捕获阶段事件委托”轻松解决的任务(不幸的是不经常使用)。

      一个事件监听器只在顶部节点之一上添加一次,可以是“document”节点本身或“html”元素或“body”元素,因此在一个始终存在于 DOM 之后的元素上初始页面加载。

      此元素将负责捕获页面上的所有点击,当前存在或稍后加载(通过您的 Ajax 请求),并且此元素将在页面的整个生命周期内以这种方式运行,不会需要设置/销毁事件,因为我看到了一些建议。好吧,这可能也可以,但会在浏览器引擎上产生更多的工作量,消耗更多的内存,需要更多的代码,并且总体上会变慢并且至少可以说是不必要的棘手。

      代码示例

      我已经为你设置了两个 CodePen 示例:

      Delegation1 在 HEAD 部分有事件处理代码,如果没有限制或其他未知的怪癖存在,这是我处理此类问题的首选方式。此版本的好处是更短/更快且更易于维护的代码。

      Delegation1

      Delegation2 在 Ajax 响应中有事件处理代码,应该满足 HTML/JS 持续加载的要求。有一个检查,只安装一次监听器,避免多个事件竞争和重叠。

      Delegation2

      示例中的 Javascript 代码包含相关的 cmets,应该足以解释实现的策略,以使其尽可能简单并在其他类似情况下可重用。

      这些示例是针对旧版浏览器编写的,新版浏览器公开了更强大的 API,例如 element.matches(selector),对于事件委托非常有用。我故意避免使用它来获得更广泛的支持。

      【讨论】:

        【解决方案5】:

        您需要从 HTML 中去除

        var $html = $(data.Html)
        

        然后,像这样拉出所有脚本标签:

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

        那么你需要在将它附加到DOM之前从html中删除所有标签。

        $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】:

          为避免在不实现平面计时器的情况下出现竞态条件的可能性,您将需要返回一个将脚本与 html 分开的 ajax 响应,例如返回两个元素(脚本或脚本数组)的 json 对象以支持您需要多个以及包含字符串化 html 标记的第二个元素的情况。

          来自 ajax 的示例 json 响应:

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

          为了简洁起见,这个(解析的)json 在下面将被称为xhrResponse

          在应用到 dom 之前,请在加载 dom 元素之前为您的脚本附加 a preload tag,这样它们将在不加载的情况下在后台开始解析,这应该最大限度地减少整体加载时间,并有助于缓解很可能会出现持续的比赛条件:

          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 标记提供模板,则更多问题),或者如果最终用户当前可用的内存不足(旧机器、打开的大量选项卡或非常大的 html 有效负载,即使直接提供字符串化)。

          请参阅this answer 以获得一个非常强大的解决方案来检查动态添加的 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);
          

          前面提到的预加载标签应该确保您的脚本已经被浏览器本地化,或者已经接近完成。然后,您可以使用就绪状态事件侦听器检查它是否已完成,如果它仍然给您带来问题:

          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 参数调用中,一切都会好的:

            异步:假

            【讨论】:

              【解决方案8】:

              我认为可能的问题是: 1.包含和排除触发相同的功能。处理两者的内部逻辑错误。 (因为您告诉它有 50% 的时间有效)。 2.添加 HTML 后尝试更新 DOM。

                 componentHandler.upgradeDom();
              

              紧接着

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

              希望对你有帮助!!!

              【讨论】:

                猜你喜欢
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 2015-10-20
                • 2021-10-27
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                • 1970-01-01
                相关资源
                最近更新 更多