Как изменить HTML-контент при его загрузке на страницу
Я делаю A/B-тестирование на нашем сайте, и большую часть своей работы я выполняю в виде файла JS, который загружается вверху страницы перед тем, как что-либо еще отображается, но после загрузки jQuery, что иногда бывает удобно.
Взяв очень простой пример изменения тега H1, я обычно вставляю стиль в голову, чтобы установить непрозрачность H1 в 0, а затем в DOMContentLoaded я манипулирую содержимым H1, а затем устанавливаю непрозрачность в 1. Причина этого заключается в том, чтобы избежать изменения старого содержимого до того, как произойдут изменения - скрытие всего объекта более изящно на глаз.
Я начал смотреть на API MutationObserver. Я использовал это раньше, когда менял содержимое в диалоговом окне наложения, которое пользователь мог открыть, что кажется довольно классным подходом, и мне интересно, удалось ли кому-нибудь использовать MutationObserver для прослушивания документа при его первой загрузке / анализировать и вносить изменения в документ перед первым рендерингом и перед DOMContentLoaded?
Этот подход позволил бы мне изменить содержимое H1 без необходимости скрывать его, изменять, а затем показывать.
Я пытался, но до сих пор потерпел неудачу, и только что закончил тем, что читал об устаревших мутационных событиях и задавался вопросом, пытаюсь ли я сделать что-то, что просто невозможно. Однако нам (не мне) удалось разместить робота на Марсе, поэтому я надеюсь, что смогу решить эту проблему.
Так можно ли использовать MutationObservers для изменения содержимого HTML на лету, когда страница загружается / анализируется?
Спасибо за любую помощь или указатели.
С уважением, Ник
2 ответа
Документы по MDN имеют общий неполный пример и не показывают типичных ошибок. Библиотека итоговых данных об мутациях обеспечивает удобную оболочку для человека, но, как и для всех оболочек, она добавляет накладные расходы. См. Производительность MutationObserver для обнаружения узлов во всем 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, mutation =>
Array.prototype
.filter.call(mutation.addedNodes, added =>
added.localName == 'h1' && added.textContent.match(/foo/)
).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, lenAdded = added.length; j < lenAdded; j++) {
var node = added[j];
var found;
if (node.localName === 'h1') {
found = [node];
} else if (node.children && node.children.length) {
found = node.getElementsByTagName('h1');
} else {
continue;
}
for (var k = 0, lenFound = found.length; k < lenFound; k++) {
var h1 = found[k];
if (!h1.parentNode || !h1.textContent.match(/foo/)) {
// either removed in another mutation or has no text to replace
continue;
}
var walker = document.createTreeWalker(h1, NodeFilter.SHOW_TEXT);
while (walker.nextNode()) {
var textNode = walker.currentNode;
var text = textNode.nodeValue;
if (text.match(/foo/)) {
textNode.nodeValue = text.replace(/foo/, 'bar');
}
}
}
}
}
}
Почему уродливая ваниль for
петли? Так как forEach
а также filter
и ES2015 for (val of array)
очень медленные в сравнении. См. Производительность MutationObserver для обнаружения узлов во всем DOM.
Почему TreeWalker? Для сохранения любых слушателей событий, прикрепленных к подэлементам. Чтобы изменить только Text
узлы: у них нет дочерних узлов, и их изменение не вызывает новую мутацию, потому что мы использовали childList: true
не characterData: true
,
Обработка относительно редких элементов с помощью живой коллекции HTMLC без перечисления мутаций.
Поэтому мы ищем элемент, который предполагается использовать редко, например, тег H1, или IFRAME и т. Д. В этом случае мы можем упростить и ускорить обратный вызов наблюдателя с помощью автоматически обновляемого HTMLCollection, возвращаемого getElementsByTagName.
var h1s = document.getElementsByTagName('h1');
function onMutation(mutations) {
if (mutations.length == 1) {
// optimize the most frequent scenario: one element is added/removed
var added = mutations[0].addedNodes[0];
if (!added || (added.localName !== 'h1' && !added.children.length)) {
// 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, len = h1s.length; i < len; i++) {
var h1 = h1s[i];
if (!h1.textContent.match(/foo/)) {
continue;
}
var walker = document.createTreeWalker(h1, NodeFilter.SHOW_TEXT);
while (walker.nextNode()) {
var textNode = walker.currentNode;
var text = textNode.nodeValue;
if (text.match(/foo/)) {
textNode.nodeValue = text.replace(/foo/, 'bar');
}
}
}
}
Я делаю A/B-тестирование на жизнь и довольно часто использую MutationObservers с хорошими результатами, но гораздо чаще я просто делаю длинные опросы, что на самом деле и делают большинство сторонних платформ, когда вы используете их WYSIWYG (или иногда даже их редакторы кода). Цикл 50 миллисекунд не должен замедлять страницу или вызывать FOUC.
Я обычно использую простой шаблон, такой как:
var poller = setInterval(function(){
if(document.querySelector('#question-header') !== null) {
clearInterval(poller);
//Do something
}
}, 50);
Вы можете получить любой элемент DOM, используя селектор sizzle, как в jQuery с document.querySelector, который иногда является единственной вещью, для которой вам в любом случае нужна библиотека.
На самом деле мы делаем это так часто на моей работе, что у нас есть процесс сборки и библиотека модулей, которая включает функцию " Когда", которая делает именно то, что вы ищете. Эта конкретная функция проверяет jQuery так же, как и элемент, но было бы тривиально изменить библиотеку, чтобы она не зависела от jQuery (мы полагаемся на jQuery, так как она находится на большинстве сайтов нашего клиента и мы используем ее для множества вещей).
Говоря о сторонних платформах тестирования и библиотеках javascript, в зависимости от реализации, многие платформы (такие как Optimizely, Qubit и, я думаю, Monetate) поставляют версию jQuery (иногда сокращенную), которая доступна сразу при выполнении вашего кода. Так что это то, на что стоит обратить внимание, если вы используете стороннюю платформу.