【问题标题】:Client-Side Dynamic Removal of <script> Tags in <head>客户端动态删除 <head> 中的 <script> 标签
【发布时间】:2012-09-26 17:12:30
【问题描述】:

是否可以在 HTML 文档客户端的 &lt;head&gt; 中删除脚本标签,然后再执行这些标签?

在服务器端,我可以在&lt;head&gt; 中的所有其他&lt;script&gt; 标记之上插入一个&lt;script&gt;,除了一个,我希望能够删除所有后续脚本。我无法从服务器端删除 &lt;script&gt; 标签。

我尝试过的:

(function (c,h) {
  var i, s = h.getElementsByTagName('script');
  c.log("Num scripts: " + s.length);
  i = s.length - 1;
  while(i > 1) {
    h.removeChild(s[i]);
    i -= 1;
  }
})(console, document.head);

但是,记录的脚本数量只有 1 个,因为(正如@ryan 指出的)代码是在 DOM 准备好之前执行的。尽管将上面的代码包装在 document.ready 事件回调中确实可以正确计算 &lt;head&gt; 中的 &lt;script&gt; 标记的数量,但等待 DOM 准备好无法阻止脚本加载。

是否有可靠的方法在 DOM 准备好之前操作 HTML?

背景

如果您想要更多上下文,这是尝试合并脚本的一部分,其中没有可用的服务器端聚合选项。许多正在加载的 JS 库来自配置选项有限的 CMS。内容大多是静态的,因此无需担心手动聚合 JavaScript 并从不同位置提供它。也欢迎任何有关替代适用聚合技术的建议。

【问题讨论】:

  • 是的,根据最后一行传递给匿名函数的参数。 c = console; h = document.head
  • 所以问题出在这里:您已将一个脚本作为第一个脚本插入到页面中。由于 js 脚本一旦加载就会触发(这就是 document.ready 的原因 - 确保在 js 触发之前文档已完全加载)您的脚本正在运行,发现自己并完成,因为 s.length 是 1 和 i已经比它大了。
  • 尝试将其作为最后一个脚本插入头部。它仍然应该在 dom 的其余部分准备好之前触发,即在加载 html 之前。它还应该在头脑中拾取其他脚本。然后让它删除除最后一个以外的所有内容,例如for(i = 0; i &lt; s.length -1; i++)
  • 你有没有试过我上面的评论。至于另一种方式。您必须在要加载的页面的 dom 之外使用某些东西。使用 chrome 扩展程序或 firefox 插件。使用 chrome 扩展,我将使用 chrome api 获取有关 url 更改的传入信息,剥离
  • @RobW IE7 将是首选,但如果这是一个好的答案,我不会因为不支持那么低而拒绝它。指示可能可行的方法 比给定答案中的特定浏览器支持更重要。关键是要防止通过&lt;head&gt; 顶部的脚本评估页面上的所有脚本,这可能会抑制或删除后续脚本。

标签: javascript html browser aggregation


【解决方案1】:

由于您无法阻止将来的&lt;script&gt; 标记评估(只要找到&lt;/script&gt; 标记,就会获取并评估&lt;script&gt; 的相应代码。&lt;script src&gt; 将阻止文档进一步加载直到源除非设置了 async 属性,否则将被提取),则需要采用不同的方法。
在我提出解决方案之前,我问:什么可以阻止 &lt;script&gt; 标记中的脚本执行?确实,

  1. 从源代码中删除&lt;script&gt;
  2. 添加 Content Security policy 指令以阻止来自某些来源的脚本。
  3. 触发(运行时)错误。

1 很明显,2 可以从文档中推导出来,所以我将重点放在 3 上。下面的示例很明显,需要根据实际用例进行调整。

代理

这是代理现有方法的一般模式:

(function(Math) {
   var original_method = Math.random;
   Math.random = function() {
       // use arguments.callee to read source code of caller function
       if (/somepattern/.test(arguments.callee.caller)) {
           Math.random = original_method; // Restore (run once)
           throw 'Prevented execution!';
       }
       return random.apply(this, arguments); // Generic method proxy
   };
})(Math);
// Demo:
function ok()    { return Math.random(); }
function notok() { var somepattern; return Math.random(); }

在此示例中,代码阻止程序仅运行一次。可以去掉恢复行,或者添加var counter=0;if(++counter &gt; 1337)在1337调用后恢复方法。

arguments.callee.callernull 如果调用者不是函数(例如顶级代码)。不是灾难,您可以从参数或this 关键字或任何其他环境变量中读取,以确定是否必须停止执行。
演示:http://jsfiddle.net/qFnMX/

拒绝设置器/获取器

以下是破坏二传手的一般模式:

Object.defineProperty(window, 'undefinable', {set:function(){}});
/*fail*/ function undefinable() {} // or window.undefinable = function(){};

演示:http://jsfiddle.net/qFnMX/2/

当然还有吸气剂:

(function() {
    var actualValue;
    Object.defineProperty(window, 'unreadable', {
        set: function(value) {
            // Allow all setters for example
            actualValue = value;
        },
        get: function() {
            if (/somepattern/.test(arguments.callee.caller)) {
                // Restore, by deleting the property, then assigning value:
                delete window.unreadable;
                window.unreadable = actualValue;
                throw 'Prevented execution!';
            }
            return actualValue;
        },
        configurable: true // Allow re-definition of property descriptor
    });
})();
function notok() {var somepattern = window.unreadable; }
// Now OK, because 
function nowok() {var somepattern = window.unreadable; }
function ok()    {return unreadable;}

演示:http://jsfiddle.net/qFnMX/4/

等等。查看要阻止的脚本的源代码,您应该能够创建特定于脚本(甚至是通用)的脚本破坏模式。

错误触发方法的唯一缺点是错误记录在控制台中。对于普通用户来说,这应该不是问题。

【讨论】:

  • +1 对备选方案的出色分析和介绍。我将玩一下这个,看看让通用工作变得多么困难。
  • +1 好主意...您还必须希望在脚本开始时可以代理一些东西,尽管这会导致运行时错误...否则您将拥有在停止之前执行的部分脚本。如果所有脚本都依赖于 jQuery 就绪/加载侦听器(或类似的东西),这将是一个非常简单的解决方案 - 尽管我怀疑是否会问这个问题;)
  • content-security-policy 指令真的很有趣!以前没听说过...
  • @pebbl 虽然不错,但脚本不必从头开始。有趣的部分通常是网络请求和输出处理,它们很容易被捕获。
  • 我选择这个作为接受的答案,即使我选择了@pebbl's answer 来奖励赏金。我的理由是,我认为这个答案提供了对手头问题的更广泛概述,因此对于未来有类似问题的 StackOverflow 用户来说可能是一个更好的起点。
【解决方案2】:

好吧,我有另一个比我的第一个想法稍微不那么疯狂,但它确实取决于您对能够在页面头部插入标签的控制:

要求

简单地说,如果您可以像下面那样在头部的任何 &lt;script&gt; 声明之前插入一个 &lt;noscript&gt; 标记,然后您可以将一个 &lt;/noscript&gt; 标记附加到头部的末尾,以及最终脚本 sn-p - 在将 noscript 标记之间的标记写回页面之前,您应该能够做任何您想做的事情。

这种方法的好处是禁用脚本的代理只会忽略并解析标记,但启用脚本的代理会将内容存储起来但不使用它...究竟是什么需要。

实现

虽然它被设计为与头部一起使用,但它可以很容易地在身体中以相同的方式使用,尽管它必须是一个单独的实现。这是因为它必须使用平衡且完整的节点树,因为标签 的性质(除非您可以设法将整个标记包装在 noscript 中?!?)

优点/缺点

它不是完全证明的,因为脚本可以位于头部和身体标签之外 - 至少在它们被解析之前 - 但它似乎对我迄今为止测试过的所有东西都非常自信...... 它不依赖于少量随机的 ajax 驱动的代码,这些代码会在浏览器更新的第一个迹象时中断;)

另外我也喜欢在 noscript 标签中使用脚本标签的想法......

<head>
<meta http-equiv="content-type" content="text/html; charset=utf-8" />
<noscript id="__disabled__">
  <script src="jquery.js"></script>
  <title>Another example</title>
  <script>alert(1);</script>
  <link rel="stylesheet" type="text/css" href="core.css" />
  <style>body { background: #ddd; }</style>
</noscript>
<script>
(function(){
  var noscript = document.getElementById('__disabled__');
  if ( noscript ) {
    document.write(
      String(noscript.innerHTML)
        /// IE entity encodes noscript content, so reverse
        .replace(/&gt;/gi,'>')
        .replace(/&lt;/gi,'<')
        /// simple disable script regexp
        .replace(/<script[^>]*>/gi,'<'+'!--')
        .replace(/<\/script>/gi,'//--'+'>')
    );
  }
})()
</script>
</head>

【讨论】:

  • 谢谢 :) 希望对您的想法有所帮助。
【解决方案3】:

您可以尝试使用DOM Mutation 事件:

DOMAttrModified
DOMAttributeNameChanged
DOMCharacterDataModified
DOMElementNameChanged
DOMNodeInserted
DOMNodeInsertedIntoDocument
DOMNodeRemoved
DOMNodeRemovedFromDocument
DOMSubtreeModified

像这样:

document.head.addEventListener ('DOMNodeInserted', function(ev) {
   if (ev.target.tagName == 'SCRIPT') {
       ev.target.parentNode.removeChild(ev.target);
   }
}, false);

你也可以通过MutationObserver尝试新的方法

【讨论】:

  • 这很酷,我以前也用过,但它并不是特别有用,除非你从不打算让人们使用即访问你的网站。据我所知,dom 突变仅在 chrome+safari 中实现。
  • +1 非常有趣;以前不知道这些。然而,我想知道,因为如果脚本在事件触发时已经执行,那么这些事件都不能取消。
【解决方案4】:

好的,所以我还没有在 Internet Explorer 中测试任何这些(我怀疑它会起作用),不要因为黑客的可怕性而责备我......我知道; ) 但它似乎在 Mac OSX 上的 FireFox、Safari、Chrome 和 Opera 中有效——至少是这些用户代理最近的公开版本。当我可以访问Windows机器时,我会看看我是否可以改进它......虽然我对IE没有太大希望。

(function(xhr,d,de){
  d = document;
  try{
    de = ((de = d.getElementsByTagName('html')[0]) 
      ? de : ( d.documentElement ? d.documentElement : d.body ));
    /// this forces firefox to reasses it's dom
    d.write('&nbsp;');
    /// make an ajax request to get the source of this page as a string
    /// this could be improved, I've just chucked it in as an example
    if (window.XMLHttpRequest) {
      xhr = new window.XMLHttpRequest;
    }else{
      xhr = new ActiveXObject("MSXML2.XMLHTTP");
    }
    if ( xhr ) {
      /// open non-async so the browser has to wait
      xhr.open('GET', window.location, false);
      xhr.onreadystatechange = function (e,o,ns){
        /// when we've got the source of the page... then
        if ((o = e.target) && (o.readyState == 4) && (o.status == 200)) {
          /// remove the script tags
          window.ns = ns = String(o.responseText)
              .replace(/<script[^>]*>/gi,'<'+'!--')
              .replace(/<\/script>/gi,'//--'+'>');
          /// fix for firefox - this causes a complete 
          /// rewrite of the main docelm
          if ( 'MozBoxSizing' in de.style ) {
            de.innerHTML = ns;
          }
          /// fix for webkit, this seems to work, whereas 
          /// normal document.write() doesn't. Probably 
          /// because the window.location resets the document.
          else {
            window.location = 'javascript:document.write(window.ns);';
          }
        }
      };
      xhr.send({});
    }
  }
  catch(ex){}
})();

只是说我已经用我能想到的几乎所有类型的脚本标签测试了这个,放在我可以放置它们的任何地方。而且我还没有一个人设法突破。正如我所说,有趣的问题......虽然我不知道上述在生产环境中的运行情况:S ;)

基本上,这必须作为脚本标记放置在 head 标记的顶部。

一个测试例子:

http://pebbl.co.uk/stackoverflow/12748067.html

【讨论】:

  • 上帝之母...好一个 xD 我担心这个黑客,因为当你一个月前尝试做一件正常的事情时,它们通常会再次弹出
  • 是的 :) 正如我所说...我自己根本不会依赖它...只需要 Firefox 开始忽略第一个 document.write() (作为另一个浏览器会这样做) 并且js会再次突破。如果它仅用于脚本聚合 - 而不是删除 - 它可能是一种可能性......但是聚合通常作为优化完成,并且两次调用页面源 (除非大量缓存) 不是将是最佳的。
  • +1 绝对是一种有趣而聪明的方法!我从来没有想过重新请求页面,然后用过滤后的响应覆盖文档。但是,也同意在生产中使用它的可行性。不过,到目前为止,您已经得到了最好的答案;我还在等着看有没有其他人有替代品。
  • @merv 谢谢,呵呵,是的,不用担心.. 可能还有一个更健全的客户端替代方案。关于在服务器端没有控制权......你不可能创建一个 .htaccess 重新路由器 - 或类似的东西吗? .. 与 Drupal 或 MVC 系统的做法类似。服务器上的任何请求都通过一个索引进行路由,该索引为所请求的实际页面提取代码,使用某种形式的缓冲区捕获输出,然后在发送给用户之前剥离/聚合脚本标签/脚本?
【解决方案5】:

不可以

我现在找不到官方文档,但是当我从Nicholas Zakas 阅读High Performance Javascript 时,当渲染引擎找到一个标记脚本时,它会停止 HTML 渲染(因此不会创建其他节点),下载脚本并执行它。然后它继续呈现 HTML。这就是为什么当您在标签上执行“document.write()”时,结果会被添加到标签之后,然后呈现页面的其余部分。

(不知道能不能在这里插入一段书……)

所以它不像渲染页面,然后你删除节点并且脚本不会被执行,当浏览器找到一个标签你不能做任何事情,直到这段代码被执行。

我们在产品中遇到了一个非常相似的问题,我们向 DOM 添加了一个脚本标签,我们需要在新标签开始执行之前执行一些代码,经过一周的研究,我们不得不找到另一个解决方案。

抱歉,但我希望您不要像我们那样浪费太多时间。无论如何,我会继续寻找浏览器规范。

【讨论】:

    猜你喜欢
    • 2014-12-05
    • 2014-03-23
    • 2022-12-05
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-04-10
    • 1970-01-01
    • 2011-03-06
    相关资源
    最近更新 更多