How to change the HTML content as it's loading on the page

我与影子孤独终老i 提交于 2019-11-27 02:15:35

The docs on MDN have a generic incomplete example and don't showcase the common pitfalls. Mutation summary library provides a human-friendly wrapper, but like all wrappers it adds overhead. See Performance of MutationObserver to detect nodes in entire DOM.

Create and start the observer.

Let's use a recursive document-wide MutationObserver that reports all added/removed nodes.

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

Naive enumeration of added nodes.

Slows down loading of enormously big/complex pages, see Performance.
Sometimes misses the H1 elements coalesced in parent container, see the next section.

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

Efficient enumeration of added nodes.

Now the hard part. Nodes in a mutation record may be containers while a page is being loaded (like the entire site header block with all its elements reported as just one added node): the specification doesn't require each added node to be listed individually, so we'll have to look inside each element using querySelectorAll (extremely slow) or getElementsByTagName (extremely fast).

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

Why the ugly vanilla for loops? Because forEach and filter and ES2015 for (val of array) are very slow in comparison. See Performance of MutationObserver to detect nodes in entire DOM.

Why the TreeWalker? To preserve any event listeners attached to sub-elements. To change only the Text nodes: they don't have child nodes, and changing them doesn't trigger a new mutation because we've used childList: true, not characterData: true.

Processing relatively rare elements via live HTMLCollection without enumerating mutations.

So we look for an element that is supposed to be used rarely like H1 tag, or IFRAME, etc. In this case we can simplify and speed up the observer callback with an automatically updated HTMLCollection returned by 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');
            }
        }
    }
}

I do A/B testing for a living and I use MutationObservers fairly often with good results, but far more often I just do long polling which is actually what most of the 3rd party platforms do under the hood when you use their WYSIWYG (or sometimes even their code editors). A 50 millisecond loop shouldn't slow down the page or cause FOUC.

I generally use a simple pattern like:

var poller = setInterval(function(){
  if(document.querySelector('#question-header') !== null) {
    clearInterval(poller);

    //Do something
  }
}, 50);

You can get any DOM element using a sizzle selector like you might in jQuery with document.querySelector, which is sometimes the only thing you need a library for anyway.

In fact we do this so often at my job that we have a build process and a module library which includes a function called When which does exactly what you're looking for. That particular function checks for jQuery as well as the element, but it would be trivial to modify the library not to rely on jQuery (we rely on jQuery since it's on most of our client's sites and we use it for lots of stuff).

Speaking of 3rd party testing platforms and javascript libraries, depending on the implementation a lot of the platforms out there (like Optimizely, Qubit, and I think Monetate) bundle a version of jQuery (sometime trimmed down) which is available immediately when executing your code, so that's something to look into if you're using a 3rd party platform.

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!