确定元素是否由 JS 与原始 HTML doc *或* 检测脚本何时通过 InnerHtml 更新节点

Determine whether element was added by JS vs original HTML doc *OR* detect when a script updates a node by InnerHtml

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

实际用例:

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

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


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

有没有人有什么想法?

如果 脚本依赖于 jQuery 这很简单,你可以使用 $.holdReady() 来延迟 ready 事件的触发,直到你的观察者正在倾听。

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

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


不过不用说,假设其他脚本依赖于 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 已经存在并且我们可以在正确的时间开始收听。
我们假设所有其他脚本在加载或就绪事件触发后开始修改 DOM。如果不是这种情况,请相应地移动脚本块,以便此脚本在 DOM 解析结束时触发,而其他脚本在此之后触发。

我还没有对此进行彻底的测试,但这应该可以帮助您入门。

变异观察者应该(主要)基于以下假设工作​​:

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

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

由于您可以完全控制您的浏览器,因此您可以通过 DOMWindowCreated events in privileged code and/or frame scripts 尽早执行您自己的脚本。

不幸的是,使用突变观察器的建议不适用于这种情况。突变观察者不知道 为什么 将 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 与 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 让我走上这条路