确定元素是否由 JS 添加,还是原始 HTML 文档 *OR* 检测脚本何时通过 InnerHTML 更新节点



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

实际用例:

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

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


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

有人有什么想法吗?

不幸的是,使用突变观察者的建议不适用于这种情况。 突变观察者不知道为什么将 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),调用将在内部HTML调用上失败,而不是在属性定义上失败。

在浏览器中处理原型:

我在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 让我走上这条路

如果脚本依赖于 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);
}());

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

我还没有彻底测试过,但这应该让你开始。

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

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

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

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

相关内容

最新更新