此文为翻译,原文:http://james.padolsey.com/javascript/replacing-text-in-the-dom-its-not-that-simple/
查找和替换DOM中的文本是个比较棘手的问题,特别是要正确的彻底解决,又不具侵害性(意思就是说最小化DOM侵害,对文本节点不做多余的格式化,比如将分隔的文本节点连接起来)。
update:本文中最后提到问题,后面有了解决办,查看原文
实际上,我讨论的是在页面上找一段特殊的文本或者匹配某个模式的文本然后对它做写处理,比如用一个html元素包裹起来或者做点其它的改变。
(丫的,老外写文章真啰嗦....翻译真累啊。)
试想下,现在又下面一个段落。
<p>This order's reference number is RF83297.</p>
我们要找到引用编号,并用一个连接标签把它包括起来,让别人可以跳转到想去的页面。看起来似乎比较简单:
(用了jquery)
jQuery('p').each(function(){
var p = $(this);
p.html(
p.text().replace(
/\bRF\d{5}/g,
'<a href="/order/$&">$&</a>'
)
)
});
这样处理问题很明显,如果p里面有其它标签的话就悲剧了,会被替换掉,而且会盲目的搜索空的p标签。
比如:
<p>
<a href="/admin">Go back to admin!</a><br/>
This order's reference number is RF83297.
</p>
替换后就会变成:
<p>
Go back to admin!
This order's reference number is <a href="/order/RF83297">RF83297</a>.
</p>
同样,p.html( p.html.replace(...) ) 这种方式肯定也是不行的,p里面的DOM会重新生成,之前绑定的事件就不存在了。
正确的做法
不管你还不喜欢,正确的方式应该是获取所有的文本节点来处理,文本节点跟element node处理方式是一样的,但是有些关键的地方在于:
*文本节点不会有子节点
*对于文本节点,你想知道的所有信息都在data(或者nodeValue)属性里
*文本节点不会有事件,不会包含样式,除了包括文本内容外基本上是一无是处
上面这段html
<p>
<a href="/admin">Go back to admin!</a><br/>
This order's reference number is RF83297.
</p>
它对应的DOM结构是:
-> P ELEMENT
-> TEXT NODE (data: "\n ")
-> A ELEMENT (href: "/admin")
-> TEXT NODE (data: "Go back to admin!")
-> BR ELEMENT
-> TEXT NODE (data: "\n This order's reference number is RF83297.\n")
我们并不知道哪个文本节点力包含了我们要找的引用编码所以只能遍历所有节点去尝试:
// Pretending we have more than one paragraph to look through
jQuery('p').each(function(){
traverseChildNodes(this);
});
function traverseChildNodes(node) {
var next;
if (node.nodeType === 1) {
// (Element node)
if (node = node.firstChild) {
do {
// Recursively call traverseChildNodes
// on each child node
next = node.nextSibling;
traverseChildNodes(node);
} while(node = next);
}
} else if (node.nodeType === 3) {
// (Text node)
if (/\bRF\d{5}/.test(node.data)) {
// Do something interesting here
alert('FOUND A MATCH!');
}
}
}
查找匹配并不难,但是要把匹配的文本替换成一个连接就比较麻烦了。我们可以把要替换的结果用innerHTML塞到一个临时创建的DOM标签里,然后把这个dom节点里的子节点一个一个的添加到要替换的位置。
创建一个wrapMatchesInNode 方法用来替换和插入结果:
// ....
} else if (node.nodeType === 3) {
// (Text node)
if (/\bRF\d{5}/.test(node.data)) {
wrapMatchesInNode(node);
}
}
// ....
wrapMatchesInNode:
运行后的结果:
function wrapMatchesInNode(textNode) {
var temp = document.createElement('div');
temp.innerHTML = textNode.data.replace(/\bRF\d{5}/g, '<a href="/order/$&">$&</a>');
// temp.innerHTML is now:
// "\n This order's reference number is <a href="/order/RF83297">RF83297</a>.\n"
// |_______________________________________|__________________________________|___|
// | | |
// TEXT NODE ELEMENT NODE TEXT NODE
// Extract produced nodes and insert them
// before original textNode:
while (temp.firstChild) {
console.log(temp.firstChild.nodeType);
textNode.parentNode.insertBefore(temp.firstChild, textNode);//他妹的,原文中代码写错了,这里修正了下
}
// Logged: 3,1,3
// Remove original text-node:
textNode.parentNode.removeChild(textNode);
}
运行后的结果:
<p>
<a href="/admin">Go back to admin!</a><br/>
This order's reference number is <a href="/order/RF83297">RF83297</a>.
</p>
看起来还不错....
但是 ,我们现在面临的其它问题...
一块文本在DOM结构中并不能确保是在一个独立的文本节点中,连续的文本字符串也会被浏览器解析成一个DOM节点。
<div>
Nothing but text.
No elements.
No comments.
Nada...
</div>
上面的文本会被解析成一个文本节点。但是如果额外的往这个div节点里append文本时,不会被合并到已经存在的文本节点中,除非是用innerHTML/innerText (/textContent) 之类的方式。
所以,很有可能我们想要找的引用编码被放在了多个文本节点中,比如:
-> TEXT NODE (data: RF)
-> TEXT NODE (data: 832)
-> TEXT NODE (data: 97)
上面我们使用的方式对于这种情况就无效了。
在找到解决方式之前我们可以考虑下下面这些问题,假设:
<span>RF</span>83297
文本被分隔开了,一部分被包裹在一个标签里,(此处略去xx个字,原文的废话我就不写了...丫的)
如果我们去匹配一个字符串,不管它其中一部分被任何标签包括了,然后把它替换成一个连接,或者用其它元素包裹起来,就有肯能出现:
<p>
...
This order's reference <em>number is RF</em>83297.
</p>
把RF</em>83297放到一个<a>...</a>里就破坏了HTML结构
唯一可行的办法,在我看来,就是忘记匹配字符被标签隔断的思路(像<em>RF</em>83297),仅尝试文本节点的data,把相邻的标签的文本节点合并在一起,然后再做查找匹配。
坑爹啊,看完了这文章最后说了句“ solution… I simply don’t have one!”, 不过还好作者写了续集,有解决的方案了。