【问题标题】:Determine whether element was added by JS vs original HTML doc *OR* detect when a script updates a node by InnerHtml确定元素是否由 JS 添加与原始 HTML 文档 *或* 检测脚本何时通过 InnerHtml 更新节点
【发布时间】:2015-03-07 05:14:48
【问题描述】:

简而言之,我需要知道页面上的某些元素是否在页面上,因为某个脚本通过父元素上的 InnerHtml 属性插入了它们,或者它们是否是下载的原始 HTML 文档的一部分。这两种可能性在这个(荒谬的)应用程序中意味着非常不同的东西。

实际用例:

第 3 方脚本通过设置元素的 InnerHtml 属性来更新页面上的随机节点元素。我可以完全控制浏览器(WPF / GeckoFx / XulRunner),并且能够随意注入和修改(新)JS,但没有洞察力或修改严重混淆的第 3 方脚本的能力。

获取我需要的数据的唯一方法是在页面加载后确定屏幕上的某些元素(如果存在)是否由第三方脚本(innerHtml)加载,或者它们是否是原始元素的一部分3rd 方脚本运行前的 html 文档。


简单地将页面的原始 html 内容源与其最终状态进行比较是很困难的,因为原始页面上有很多内联脚本。

有人有什么想法吗?

【问题讨论】:

  • 我相信变异观察者应该做的伎俩
  • @Xotic750 我无法让它工作。我在任何其他脚本触发之前注入了我的脚本,并跟踪了所有突变事件(nodeAdded 类型)。当浏览器解析 html 文档时,它会向 dom 添加新节点。我想不出一种方法来区分由此产生的节点与通过 innerHtml 调用添加的节点。我在这方面的最佳想法是禁用所有 js,加载页面,听取观察者的意见,然后在启用 js 的情况下重新加载,但这看起来像是一个目标之路的雷区。
  • 我认为你的脚本需要在加载页面的head标签中,否则我担心你可能会倒霉。
  • 如果您可以在 DOM 准备好之后并且在任何其他之前运行您的脚本,您可以克隆文档正文并比较之前和之后感兴趣的节点。虽然很脏。另一个想法是用您自己的构造函数覆盖 Element 构造函数,用您选择的派发事件的 getter/setter 替换 innerHTML 属性,然后使用原始 内部HTML。只有当您“完全控制浏览器”时才可行,它不能作为通用解决方案。只是想法……
  • 这是对我父母的反思! :-)(来自Shelly Berman skit 的一行)很高兴它让你走上了正确的道路。发布您提出的内容作为对您的 OP 的回答。这种方法已经存在了一段时间(例如早期的 Prototype.js),尽管它不能被推荐为通用解决方案。

标签: javascript html firefox javascript-framework mutation-observers


【解决方案1】:

如果脚本依赖于 jQuery,这很容易,您只需使用 $.holdReady() 将就绪事件的触发延迟到您的观察者正在侦听之后。

HTML:

<h1>Sample title</h1>
<p>Sample paragraph</p>

Js:

$(function() {
    $('body').append("<p>Foo</p>").append("<p>Bar</p>");
});

(function() {
    $.holdReady(true);
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            console.log(mutation.type);
        });
    });
    var target = document.querySelector('html');
    var config = {
        childList: true,
        attributes: true,
        subtree: true,
        characterData: true
    };
    setTimeout(function() {
        observer.observe(target, config);
        $.holdReady(false);
    }, 1);
}());

如上所示,无论其他脚本在何处绑定到 ready 事件,这都会起作用。


然而,不用说,假设其他脚本依赖于 jQuery 远非我们总能指望的事​​情。如果我们正在寻找一种不管它如何都有效的解决方案,我们将不得不变得棘手。

HTML 和以前一样。
js正文结尾

$(function() {
    $('body').append("<p>Foo</p>").append("<p>Bar</p>");
});

(function() {
    var observer = new MutationObserver(function(mutations) {
        mutations.forEach(function(mutation) {
            console.log(mutation.type);
        });
    });
    var target = document.querySelector('html');
    var config = {
        childList: true,
        attributes: true,
        subtree: true,
        characterData: true
    };
    observer.observe(target, config);
}());

要获得预期的功能,请确保此脚本块绝对是正文底部的最后一个脚本块。这可以确保所有静态 DOM 都已经存在,并且我们可以在正确的时间开始监听。
我们假设所有其他脚本在 load 或 ready 事件触发后开始修改 DOM。如果不是这种情况,请相应地移动脚本块,以便此脚本在 DOM 解析结束时触发,而其他脚本在此之后触发。

我尚未对此进行彻底测试,但这应该可以帮助您入门。

【讨论】:

  • 这是我解决这个问题的初衷。我们不能假设 jQuery,(虽然也许这可以工作 x% 的时间?)。主要问题就变成了 innerHTML 使用与浏览器在页面加载时执行的相同的 Dom 解析,因此突变观察者将它们视为相同。这意味着我们实际上需要将所有 js 评估延迟到我的 js 运行之后,我对此有一些想法(首先加载页面并禁用 js,构建一个禁用所有 3rd 方脚本的浏览器插件,使用代理来延迟响应来自任何其他域等),但我有点害怕这一切有多麻烦
【解决方案2】:

变异观察者应该(大部分)基于以下假设工作​​:

  • HTML 解析器仅沿树的最底部分支附加节点。 IE。他们都应该按树顺序到达。任何不是脚本生成的东西
  • 跟踪突变观察者批次之间最后插入的节点很简单
  • .innerHTML 不仅添加节点,还删除当前子节点,尤其是经常出现的空白文本节点或 cmets,html 解析器 otoh 不应生成任何删除
  • dom 就绪事件之后的任何突变显然都已由 javascript 执行
  • 如果有疑问,可以通过将最近的唯一可识别祖先节点的内容与从 html 源生成的文档对象进行比较来仔细检查任何子树,而无需执行脚本(XMLHttpRequest 可以以文档形式而不是文本返回内容)李>
  • 在加载第 3 方脚本之前,您也可以忽略任何受信任脚本所做的任何修改,这至少可以避免一些误报。在那之后,尽管您显然无法区分哪个脚本负责修改。

因此,应该可以为突变事件构建一个分类器,以准确区分脚本生成的节点和解析器生成的节点。会有一些边缘情况,你不能确定和改进它的方法,但在不知道更多细节的情况下,我认为这可能已经足够好了。

由于您可以完全控制您的浏览器,您可以通过特权代码中的DOMWindowCreated 事件和/或frame scripts 尽早执行您自己的脚本。

【讨论】:

    【解决方案3】:

    很遗憾,使用突变观察者的建议不适用于这种情况。突变观察者不知道 为什么 一个 dom 节点被添加到页面的原因,他们只报告有一个。这意味着无法确定添加 DOM 的一部分是因为页面仍在加载,还是因为脚本已触发并动态添加内容。

    但是

    本文解释了如何覆盖 dom 中每个元素的 InnerHTML getter/setter 属性http://msdn.microsoft.com/en-us/library/dd229916(v=vs.85).aspx 由于 InnerHTML 总是由 javascript 调用,因此我知道是否使用此函数调用加载了 dom 的某个部分变得微不足道。

    虽然对于大多数应用程序来说这几乎肯定是矫枉过正并且不是一个好主意,但对于这种奇怪的情况以及 js 框架的构建来说,它可能很有意义。

    如果该文章在某个时候下线,我的初始代码类似于以下内容:

    var elem = isInIE() ? HTMLElement : Element;    // IE and FF have different inheritance models, behind the scenes.
    var proxiedInnerHTML = Object.getOwnPropertyDescriptor(elem.prototype, "innerHTML");
    
    Object.defineProperty(elem.prototype, "innerHTML", {
        set: function ( htmlContent )
        {
            // custom code goes here
    
            proxiedInnerHTML.set.call(this, htmlContent);
        }); 
    

    在旧的浏览器中应该警告,或者如果你使用了错误的元素(HTMLElement vs Element),调用将在innerHTML调用上失败,而不是在属性定义上。

    在浏览器中处理原型:

    我在 FF 和 IE 中测试了这个块,但在 Chrome 中没有。更重要的是,我发现帖子指出 w3c 规范中没有保证指定浏览器如何处理其元素类型的继承,因此不能保证 HtmlDivElement 将来或过去会调用 InnerHTML 的 HtmlElement 或 Element 基方法任何给定浏览器的版本。

    也就是说,使用所有保留的 html 关键字创建一个网页非常简单,并测试这种技术是否适用于它们。对于 IE 和 FF,截至 2015 年 1 月,此技术适用于所有领域。

    旧浏览器支持:

    虽然我没用过,但在老的浏览器中,你可以用

    document.__defineGetter__("test", /* getter function */ );
    document.__defineSetter__("test", /* setter function */ );
    document.__lookupGetter__("test");
    document.__lookupSetter__("test");
    

    感谢 RobG 让我走上这条路

    【讨论】:

      猜你喜欢
      • 2018-07-15
      • 2017-09-22
      • 1970-01-01
      • 1970-01-01
      • 2013-10-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多