如何在页面加载时更改HTML内容

9
我在我们的网站上进行A/B测试,大部分工作都在一个JS文件中进行,它在任何其他内容被渲染之前加载在页面顶部,但在jQuery加载后也很方便。
以改变H1标签为例,我通常会注入一个样式表在头部将H1透明度设置为0,然后等到DOMContentLoaded事件触发时再操作H1内容,并将透明度设置为1。做这个的原因是为了避免旧内容出现的闪烁 - 隐藏整个对象更加优雅。
我开始看MutationObserver API。我曾经在更改用户可以打开的覆盖对话框中的内容时使用过它,这似乎是一种很酷的方法。我想知道是否有人成功地使用MutationObserver来监听文档第一次加载/解析,并在第一次渲染和DOMContentLoaded之前对文档进行更改?
这种方法将允许我在不必隐藏、更改、再显示的情况下更改H1内容。
我已经尝试过但失败了,最后只是阅读了即将过时的Mutation Events,并想知道是否我正在尝试做一些不可能的事情。然而我们(不是我)已经成功将机器人送上了火星,所以我希望我能解决这个问题。
那么,使用MutationObservers在页面加载/解析时实时更改HTML内容是否可能?
感谢任何帮助或提示。
敬礼, Nick

你好@wOxxOm - 首先,对于在周日让您感到担心,我深感抱歉,但感谢您的回复。其次,您可以分享一下具体担忧的是什么吗?第三,从1到10的比例上,您有多担心?最后,如果您有来自众多易于搜索的示例中的适当资源,或许您可以将其作为答案分享,如果正确,我将会标记。感谢您的帮助。 - Nick Middleweek
我很想听听这个问题的答案,不久之前我也在看这个,但最终因为没有时间或需要而暂停了它。 - osouthgate
1
感谢您的反馈@wOxxOm - 非常有帮助,尽管前两页中没有提供有效答案的链接,但有一些好文章。如果您知道stackoverflow上的重复线程,我们可以将此问题链接到该线程作为有效的重复项。虽然很抱歉浪费了您的时间,但您非常欢迎停止回复,也许将焦点放在其他方面。祝您好运。 - Nick Middleweek
好的,谢谢...不幸的是,在我的情况下我不能使用库,但是我会看一下并感谢您提供以前回答的链接...关于“2. ...将观察者附加到文档根”的句子是我可能在这里犯错的一个很好的指标。 - Nick Middleweek
我想简化描述:https://puu.sh/r0RGg/5319a0e97e.txt 你觉得怎么样? - wOxxOm
2个回答

19

MDN文档提供了一个普通而不完整的示例,并没有展示常见的陷阱。 Mutation summary库提供了一个用户友好的封装,但就像所有的封装一样,它增加了开销。 参见Performance of MutationObserver to detect nodes in entire DOM

创建并启动观察器。

让我们使用递归的全局文档MutationObserver来报告所有添加/删除节点。

var observer = new MutationObserver(onMutation);
observer.observe(document, {
  childList: true, // report added/removed nodes
  subtree: true,   // observe any descendant elements
});

简单列举添加的节点。

会减缓极大/复杂页面的加载速度,请参见性能
有时会错过合并在父容器中的H1元素,请参见下一节。

function onMutation(mutations) {
  mutations.forEach(mutation, m => {
    [...m.addedNodes]
      .filter(node =>
        node.localName === 'h1' && /foo/.test(node.textContent))
      .forEach(h1 => {
        h1.innerHTML = h1.innerHTML.replace(/foo/, 'bar');
      });
  });
}

高效枚举添加的节点。

现在是难点。 在加载页面时,变异记录中的节点可能是容器(例如整个站点标题块及其所有元素被报告为一个添加的节点):规范不要求每个添加的节点都要单独列出,因此我们将不得不使用querySelectorAll(非常慢)或getElementsByTagName(非常快)来查看每个元素内部。

function onMutation(mutations) {
  for (var i = 0, len = mutations.length; i < len; i++) {
    var added = mutations[i].addedNodes;
    for (var j = 0, node; (node = added[j]); j++) {
      if (node.localName === 'h1') {
        if (/foo/.test(node.textContent)) {
          replaceText(node);
        }
      } else if (node.firstElementChild) {
        for (const h1 of node.getElementsByTagName('h1')) {
          if (/foo/.test(h1.textContent)) {
            replaceText(h1);
          }
        }
      }
    }
  }
}

function replaceText(el) {
  const walker = document.createTreeWalker(el, NodeFilter.SHOW_TEXT);
  for (let node; (node = walker.nextNode());) {
    const text = node.nodeValue;
    const newText = text.replace(/foo/, 'bar');
    if (text !== newText) {
      node.nodeValue = newText;
    }
  }
}

为什么要使用两个丑陋的vanilla for循环?因为在某些浏览器中,forEachfilter和ES2015 for (val of array) 可能非常慢,请参见Performance of MutationObserver to detect nodes in entire DOM

为什么要使用TreeWalker?为了保留附加到子元素的任何事件侦听器。仅更改Text节点:它们没有子节点,并且更改它们不会触发新的突变,因为我们已经使用了childList: true而不是characterData: true

通过实时HTMLCollection处理相对较少的元素而不枚举突变。

因此,我们寻找一个应该很少使用的元素,例如H1标签或IFRAME等。在这种情况下,我们可以使用由getElementsByTagName自动更新返回的HTMLCollection来简化和加速观察程序回调。

const h1s = document.getElementsByTagName('h1');

function onMutation(mutations) {
  if (mutations.length === 1) {
    // optimize the most frequent scenario: one element is added/removed
    const added = mutations[0].addedNodes[0];
    if (!added || (added.localName !== 'h1' && !added.firstElementChild)) {
      // so nothing was added or non-H1 with no child elements
      return;
    }
  }
  // H1 is supposed to be used rarely so there'll be just a few elements
  for (var i = 0, h1; (h1 = h1s[i]); i++) {
    if (/foo/.test(h1.textContent)) {
      // reusing replaceText from the above fragment of code 
      replaceText(h1);
    }
  }
}

好的,谢谢。我今晚会查看一下。不错的东西。 - Nick Middleweek
嗨,再次感谢您的回复...我正在努力理解TreeWalker的使用-您说它是为了保留附加到h1标记的子元素的任何事件侦听器,以便它不会触发新的突变,但是如果我更改文本节点的值而没有使用TreeWalker,它也不会因为使用的过滤器而触发新的突变,也不会干扰任何事件处理程序-我们只是更改textNode.nodeValue吗?干杯 - Nick Middleweek
尝试猜测正确的文本节点,而不需要递归枚举它们:1)<h1>first <span>second <a>third</a></span></h1>和2)<h1><span>first</span> second <a>third</a></h1> - wOxxOm
感谢介绍TreeWalkers,很好...我知道它可以浏览文本节点并检查每个子节点的文本值,但是关于事件侦听器和不触发另一个Mutation的评论-我假设那是针对其他答案的?不想太苛刻,只是确保我完全理解。干杯。 - Nick Middleweek
不触发另一个变异是一种优化和预防措施,它消除了需要检查节点是否在我们的代码中刚刚被改动的需求。 - wOxxOm

3
我是一名从事A/B测试工作的人,经常使用MutationObservers并取得了不错的结果,但更多时候我会采用长轮询。实际上,大多数第三方平台在您使用其所见即所得编辑器(甚至代码编辑器)时,在后台都会使用长轮询。一个50毫秒的循环不应该使页面变慢或导致FOUC。通常我会使用如下简单模式:
var poller = setInterval(function(){
  if(document.querySelector('#question-header') !== null) {
    clearInterval(poller);

    //Do something
  }
}, 50);

你可以使用类似于在jQuery中使用的document.querySelector的Sizzle选择器获取任何DOM元素,这有时是您需要库的唯一原因。
事实上,在我的工作中我们经常这样做,我们有一个构建过程和模块库,其中包括一个名为When的函数,它恰好做你要找的事情。该特定函数检查jQuery以及元素,但修改库以不依赖于jQuery将是微不足道的(我们依赖于jQuery,因为它在大多数客户网站上并且我们用它来做很多东西)。
说到第三方测试平台和JavaScript库,根据实现方式,许多平台(如Optimizely、Qubit和我认为Monetate)捆绑了一个版本的jQuery(有时是精简版),当执行代码时立即可用,所以如果您使用第三方平台,这是值得探究的。

谢谢@ Beau - 我以前用过这种模式,但有时候它并不是十分可靠。我将在这种情况下再试一次并报告结果。然而,我认为我开始更喜欢使用自然的浏览器事件,即使它会稍微减少目标受众。(jQuery-我们不会将其与我们的片段合并,我们已经在之前加载了它)。干杯。 - Nick Middleweek
1
FYI,50毫秒在60fps下是浏览器用于绘制页面的3帧。这是一个巨大的间隔(实际上1帧间隔也是明显的)。而且还有一个更大的问题:计时器回调可能会在CPU密集型复杂页面加载期间随机延迟(我见过500毫秒的延迟)。 - wOxxOm

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接