HTML重写的好替代品是什么?

6
考虑这个文档片段:
<div id="test">
    <h1>An article about John</h1>
    <p>The frist paragraph is about John.</p>
    <p>The second paragraph contains a <a href="#">link to John's CV</a>.</p>
    <div class="comments">
        <h2>Comments to John's article</h2>
        <ul>
            <li>Some user asks John a question.</li>
            <li>John responds.</li>
        </ul>
    </div>
</div>

我希望将字符串“John”替换为字符串“Peter”。这可以通过HTML重写来实现。
$('#test').html(function(i, v) {
    return v.replace(/John/g, 'Peter');    
});
工作演示:http://jsfiddle.net/v2yp5/ 上述jQuery代码看起来简单直观,但实际上这是一个糟糕的解决方案。HTML重写会重新创建#test DIV内的所有DOM节点。随后,在该DOM子树上程序性地进行更改(例如“onEvent”处理程序),或由用户(输入表单字段)进行更改时,这些更改不会被保留。
那么,应该如何执行此任务才是适当的呢?

使用 .text 而不是 .html 怎么样? - James Black
那么类似于这样的怎么样? - alex
@alex - 我还没有查看,但是 .text 应该使用 document.createTextNode 进行更改,因此任何事件处理程序(例如)都应该保持不变。 - James Black
@James text() 重写会破坏所有子元素。请参见此处 - Šime Vidas
@James 我假设 text() 使用了 innerTexttextContent - alex
@Šime Vidas - 我会使用JavaScript来完成这个任务,以确保节点不受影响。但这只是因为我不想花费太多时间在工具上进行调整。如果这太麻烦了,那就找一个可行的解决方案吧。 - James Black
8个回答

4

有没有一个jQuery插件版本可以减少一些代码呢?

http://jsfiddle.net/v2yp5/4/

jQuery.fn.textWalk = function( fn ) {
    this.contents().each( jwalk );
    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if( nn === '#text' ) {
            fn.call( this );
        } else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
            $(this).contents().each( jwalk );
        }
    }
    return this;
};

$('#test').textWalk(function() {
    this.data = this.data.replace('John','Peter');
});

或者使用一些鸭子类型的方法,添加一个选项来传递一些用于替换的字符串:

http://jsfiddle.net/v2yp5/5/

jQuery.fn.textWalk = function( fn, str ) {
    var func = jQuery.isFunction( fn );
    this.contents().each( jwalk );

    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if( nn === '#text' ) {
            if( func ) {
                fn.call( this );
            } else {
                this.data = this.data.replace( fn, str );
            }
        } else if( this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea' ) {
            $(this).contents().each( jwalk );
        }
    }
    return this;
};

$('#test').textWalk(function() {
    this.data = this.data.replace('John','Peter');
});

$('#test').textWalk( 'Peter', 'Bob' );

我想知道为什么jQuery没有提供DOM遍历功能...这对我来说似乎是非常基本的。 - Šime Vidas
@Šime: 很好的问题。使用*选择器,你可以获取所有元素,但不能获取文本节点。我猜这是因为jQuery方法通常不用于操作文本节点,而且可能会让人们感到困惑。不过,一组专门处理文本的函数似乎是有意义的。 - user113716
感谢这段优秀的代码。我需要对其进行一些修改以适应我的情况。我已经发布了详细信息作为新答案(在此给出原始代码的信用)。 - Colin Young

3
你想循环遍历所有子节点并仅替换文本节点。否则,您可能会匹配HTML、属性或任何其他序列化的内容。在替换文本时,您只想使用文本节点而不是整个HTML序列化。
我想你已经知道了 :)
Bobince有一篇很棒的JavaScript文章可以做到这一点。

@alex 我正在考虑使用Crockford的walkTheDOM函数,但我会研究bobince的代码,因为它似乎更复杂一些。 - Šime Vidas
@Šime Bobince的处理方式更适用于此,仅处理文本节点。您需要调整Crockford的处理方式以仅处理文本节点。另外,他最初的名称walk_the_DOM()去哪了?:P - alex
@alex 嗯,那是什么时候?我想那时我还是JavaScript的新手 :) - Šime Vidas
@alex,那么就是DOM遍历了... :) - Šime Vidas

2

我需要做类似的事情,但我需要插入HTML标记。我从@user113716的答案开始,并进行了一些修改:

$.fn.textWalk = function (fn, str) {
    var func = jQuery.isFunction(fn);
    var remove = [];

    this.contents().each(jwalk);

    // remove the replaced elements
    remove.length && $(remove).remove();

    function jwalk() {
        var nn = this.nodeName.toLowerCase();
        if (nn === '#text') {
            var newValue;

            if (func) {
                newValue = fn.call(this);
            } else {
                newValue = this.data.replace(fn, str);
            }

            $(this).before(newValue);
            remove.push(this)
        } else if (this.nodeType === 1 && this.childNodes && this.childNodes[0] && nn !== 'script' && nn !== 'textarea') {
            $(this).contents().each(jwalk);
        }
    }
    return this;
};

有一些隐含的假设:

  • 您始终在插入HTML。如果不是这样,您将希望添加一个检查以避免在不必要时操纵DOM。
  • 删除原始文本元素不会引起任何副作用。

1
较少侵入性的方式,但并不一定更高效,是选择您知道仅包含文本节点的元素,并使用 .text()。在这种情况下(显然不是通用解决方案):
$('#test').find('h1, p, li').text(function(i, v) {
    return v.replace(/John/g, 'Peter');
});

演示:http://jsfiddle.net/mattball/jdc87/(在单击按钮之前在<input>中输入一些内容)

由于简历链接已经损坏,因此该方法无法正常工作。 - Mottie

1

这是我会做的方式:

var textNodes = [], stack = [elementWhoseNodesToReplace], c;
while(c = stack.pop()) {
    for(var i = 0; i < c.childNodes.length; i++) {
        var n = c.childNodes[i];
        if(n.nodeType === 1) {
            stack.push(n);
        } else if(n.nodeType === 3) {
            textNodes.push(n);
        }
    }
}

for(var i = 0; i < textNodes.length; i++) textNodes[i].parentNode.replaceChild(document.createTextNode(textNodes[i].nodeValue.replace(/John/g, 'Peter')), textNodes[i]);

使用纯JavaScript编写,无需递归。


@minitech 看起来很有趣。能否在我的演示上实现这个? - Šime Vidas
@minitech +1 富有想象力的解决方案。但是真的有必要完全替换文本节点吗?设置 'textNode.nodeValue' 不应该也可以吗? - Šime Vidas
我不是很确定那个(nodeValue 不是只读的吗?),但替换文本节点应该足够有效。 - Ry-
根据DOM Core,仅当节点是只读时,设置nodeValue才会抛出异常。我不确定什么是只读节点,但对于文本节点它是有效的 - Šime Vidas

1
你可以将每个变量(例如“John”)的文本实例都包装在具有特定CSS类的span中,然后对所有这些span执行.text('..')更新。 对我来说,这似乎不太侵入性,因为DOM并没有真正被操作。
<div id="test">
    <h1>An article about <span class="name">John</span></h1>
    <p>The frist paragraph is about <span class="name">John</span>.</p>
    <p>The second paragraph contains a <a href="#">link to <span class="name">John</span>'s CV</a>.</p>
    <div class="comments">
        <h2>Comments to <span class="name">John</span>'s article</h2>
        <ul>
            <li>Some user asks <span class="name">John</span> a question.</li>
            <li><span class="name">John</span> responds.</li>
        </ul>
    </div>
</div>


$('#test .name').text(function(i, v) {
    return v.replace(/John/g, 'Peter');    
});

另一个想法是使用jQuery模板。它肯定是具有侵入性的,因为它会对DOM进行操作,并且不为此道歉。但我认为这没有什么问题...我的意思是你基本上正在进行客户端数据绑定。所以这就是模板插件的作用。

0

这似乎有效(演示):

$('#test :not(:has(*))').text(function(i, v) {
  return v.replace(/John/g, 'Peter');    
});

很遗憾,它会删除 #test 的所有后代元素,而不仅仅是它的子元素(在我的演示中是 ANCHOR、H2、UL 和 LI)。 - Šime Vidas
我意识到,如果要替换的文本位于具有子元素的元素中,例如:<li>Check out John's <a href="blog.htm">blog</a></li>,那么它将无法工作,但在您提供的示例中它可以工作。 - Mottie
抱歉在评论区里多次发言,但是简单地将名称用 span 标签包裹起来,并使用我上面发布的选择器即可再次实现:<li>查看 <span>John's</span> <a href="blog.htm">博客</a></li> - Mottie

0

POJS提供的方案还可以,但我不明白为什么要避免使用递归。通常DOM节点的嵌套不会太深,所以我认为这样做没问题。我还觉得最好构建一个单一的正则表达式,而不是在每次调用替换时使用字面量并构建表达式。

// Repalce all instances of t0 in text descendents of
// root with t1
// 
function replaceText(t0, t1, root) {

  root = root || document;
  var node, nodes = root.childNodes;

  if (typeof t0 == 'string') {
    t0 = new RegExp(t0, 'g');
  }

  for (var i=0, iLen=nodes.length; i<iLen; i++) {
    node = nodes[i];

    if (node.nodeType == 1) {
      arguments.callee(t0, t1, node);

    } else if (node.nodeType == 3) {
      node.data = node.data.replace(t0, t1);
    }
  }
}

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