使用.replace()

时间:2016-11-20 23:36:53

标签: javascript google-chrome-extension textnode

我正在开发一个Google Chrome扩展程序,允许您自动将突出显示的CSS规则应用于您选择的单词。

我有以下代码

var elements = document.getElementsByTagName('*');

for (var i=0; i<elements.length; i++) {
    var element = elements[i];

    for (var j=0; j<element.childNodes.length; j++) {
        var node = element.childNodes[j];

        if(node.nodeType === 3) {
            var text = node.nodeValue;

            var fetchedText = text.match(/teste/gi);

            if(fetchedText) {
                var replacedText = element.innerHTML.replace(/(teste)/gi, "<span style=\"background-color: yellow\">$1</span>");

                if (replacedText !== text) {
                    element.innerHTML = replacedText;
                }
            }
        }
    }
}

哪个会破坏并冻结我的Chrome标签。但是,如果我从element.innerHTML = replacedText;切换到element.innerHTML = "text";,则可行。

我似乎无法找到以下代码的错误。

2 个答案:

答案 0 :(得分:3)

您首先测试#text个节点,看看文本是否包含您要突出显示的单词,然后在父元素的.innerHTML上执行替换。这有几个问题。

  • 无限替换:当您修改父元素的.innerHTML时,您更改了childNodes数组。您可以通过在包含要替换的文本的数组中进一步添加节点的方式执行此操作。因此,当您继续扫描childNodes数组时,您总能找到包含要替换的文本的(新)节点。因此,您再次替换它,创建另一个在childNodes数组中具有更高索引的节点。无限重复。
  • 使用RegExp替换.innerHTML属性中的文本。虽然您已经测试过以确保要替换的文本实际上包含在文本节点中,但这并不妨碍您的RegExp 替换元素的实际HTML中的任何匹配单词(例如,在src="yourWord"href="http://foo.com/yourWord/bar.html",或尝试突出显示stylecolorbackgroundspanid等字词, heightwidthbuttonforminput等。)
  • 您没有检查以确保不更改<script><style>标记中的文字。
  • 您正在检查您是否仅在文本节点中进行了更改(即您检查node.nodeType === 3)。如果您没有检查此问题,则由于使用.innerHTML更改HTML,您还可能遇到以下问题:
    • 您最终可能会更改属性或实际HTML标记,具体取决于您使用.replace()更改的内容。这可能会完全破坏页面布局和功能。
    • 当您更改.innerHTML时,将完全重新创建该部分页面的DOM。这意味着元素,而新元素可能是具有相同属性的相同类型,任何附加到旧元素的事件侦听器都不会附加到新元素。这可能会严重破坏页面的功能。
    • 重复更改DOM的大部分内容对于重新呈现页面而言可能非常耗费计算量。根据您的操作方式,您可能会遇到严重的用户感知性能问题。

因此,如果要使用RegExp替换文本,则只需要对#text节点的内容执行操作,而不是在父节点的.innerHTML上执行操作。因为您想要创建其他HTML元素(例如,具有子<span style="">个节点的新#text元素),所以存在一些复杂性。

无法将HTML文本分配给文本节点以创建新的HTML节点:

无法将新HTML直接分配给文本节点,并将其评估为HTML,从而创建新节点。分配给文本节点的.innerHTML属性将在Object上创建这样的属性(就像在任何对象上一样),但不会更改屏幕上显示的文本(即实际的值) #text节点)。因此,它不会完成您想要做的事情:它不会创建父节点的任何新HTML子项。

对页面的DOM影响最小(即最不可能破坏页面上现有JavaScript)的方法是创建<span>以包含新的文本节点创建(#text节点中不在您的彩色<span>中的文本)以及您正在创建的可能多个<span>元素。这将导致使用单个#text元素替换单个<span>节点。虽然这会创建额外的后代,但它会使父元素中的子元素数保持不变。因此,任何依赖它的JavaScript都不会受到影响。鉴于我们正在更改DOM,没有办法不会破坏其他JavaScript,但这应该最小化这种可能性。

有关如何执行此操作的一些示例:请参阅this answer(替换按钮中包含这些字词的单词列表)和this answer(将所有文本放在<p>元素中,这些元素是分开的通过空格到按钮)进行完全扩展,执行正则表达式替换为新的HTML。请参阅基本相同的this answer,但会创建一个链接(它有一个不同的实现,它使用TreeWalker遍历DOM以查找#text个节点,而不是NodeIterator如在另外两个例子中所使用的那样)。

以下是代码,它将在document.body中的每个文本节点上执行您想要的替换,并创建在文本的一部分中style不同所需的新HTML: / p>

function handleTextNode(textNode) {
    if(textNode.nodeName !== '#text'
        || textNode.parentNode.nodeName === 'SCRIPT' 
        || textNode.parentNode.nodeName === 'STYLE'
    ) {
        //Don't do anything except on text nodes, which are not children 
        //  of <script> or <style>.
        return;
    }
    let origText = textNode.textContent;
    let newHtml=origText.replace(/(teste)/gi
                                 ,'<span style="background-color: yellow">$1</span>');
    //Only change the DOM if we actually made a replacement in the text.
    //Compare the strings, as it should be faster than a second RegExp operation and
    //  lets us use the RegExp in only one place for maintainability.
    if( newHtml !== origText) {
        let newSpan = document.createElement('span');
        newSpan.innerHTML = newHtml;
        textNode.parentNode.replaceChild(newSpan,textNode);
    }
}

let textNodes = [];
//Create a NodeIterator to get the text nodes in the body of the document
let nodeIter = document.createNodeIterator(document.body,NodeFilter.SHOW_TEXT);
let currentNode;
//Add the text nodes found to the list of text nodes to process.
while(currentNode = nodeIter.nextNode()) {
    textNodes.push(currentNode);
}
//Process each text node
textNodes.forEach(function(el){
    handleTextNode(el);
});

还有其他方法可以做到这一点。但是,它们将针对该特定元素(例如,父节点上的多个附加节点)对子节点的结构产生更显着的变化。这样做有可能破坏页面上已经依赖于页面当前结构的任何JavaScript。实际上,任何这样的改变都有可能打破当前的JavaScript。

此答案中的代码已从this other answer of mine

中的代码进行了修改

答案 1 :(得分:0)

我遇到的错误是由于递归循环,因为,例如,我正在寻找关键字teste,我正在插入一个内容为<span style=\"background-color: #ffff00\">teste</span>的新元素,这会强制执行脚本尝试再次替换新关键字teste,等等。

我想出了这个功能:

function applyReplacementRule(node) {
    // Ignore any node whose tag is banned
    if (!node || $.inArray(node.tagName, hwBannedTags) !== -1) { return; }

    try {
        $(node).contents().each(function (i, v) {
            // Ignore any child node that has been replaced already or doesn't contain text
            if (v.isReplaced || v.nodeType !== Node.TEXT_NODE) { return; }

            // Apply each replacement in order
            hwReplacements.then(function (replacements) {
                replacements.words.forEach(function (replacement) {
                    //if( !replacement.active ) return;
                    var matchedText = v.textContent.match(new RegExp(replacement, "i"));

                    if (matchedText) {
                        // Use `` instead of '' or "" if you want to use ${variable} inside a string
                        // For more information visit https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Template_literals
                        var replacedText = node.innerHTML.replace(new RegExp(`(${replacement})`, "i"), "<span style=\"background-color: #ffff00\">$1</span>");

                        node.innerHTML = replacedText;
                    }
                });
            }).catch(function (reason) {
                console.log("Handle rejected promise (" + reason + ") here.");
            });

            v.isReplaced = true;
        });
    } catch (err) {
        // Basically this means that an iframe had a cross-domain source
        if (err.name !== "SecurityError")
        { throw err; }
    }
}

我修改了node属性并“告诉”我已经修改了那个节点,所以我不会再次使用递归无限循环了。

P.S。如您所见,此解决方案使用jQuery。我会尝试重写这个只使用Vanilla JS。