
本文详解如何在保留 html 结构的前提下,精准按词数截断文本,并修复因空字符串导致的过早截断问题,确保“read more”展开功能稳定可靠。
本文详解如何在保留 html 结构的前提下,精准按词数截断文本,并修复因空字符串导致的过早截断问题,确保“read more”展开功能稳定可靠。
在实现基于词数的 HTML 内容截断时,一个隐蔽但关键的问题常被忽略:textContent.split(/\s+/) 会产生大量空字符串(如换行、缩进、前后空白导致的 "")。这些空字符串虽无实际语义,却被错误计入 wordCount,导致截断提前发生——例如本应显示 10 个单词,却在第 3 个有效词后就插入 ...,尤其在包含 <h1></h1>、<div> 等块级元素且内部存在缩进或换行的 HTML 中尤为明显。<h3>✅ 核心修复:跳过空字符串</h3>
<p>原逻辑中:</p><pre class="brush:php;toolbar:false;">const words = node.nodeValue.split(/\s+/);
for (let i = 0; i < words.length; i++) {
if (wordCount < limit) { // ❌ 未校验 words[i] 是否为空
truncatedContent += words[i] + ' ';
wordCount++;
} else {
truncatedContent += '... ';
return;
}
}</pre><p>✅ 正确写法需增加非空校验:</p><pre class="brush:php;toolbar:false;">for (let i = 0; i < words.length; i++) {
// 仅对非空字符串计数和拼接
if (words[i] && wordCount < limit) {
truncatedContent += words[i] + ' ';
wordCount++;
} else if (words[i]) { // 首次超限时添加省略符(注意:仅在有内容时添加)
truncatedContent += '... ';
return;
}
}</pre><blockquote>
<p>? 提示:<code>split(/\s+/) 在遇到连续空白(如 \n\t)时会生成 ["", "", "hello"],而 words[i] 为 "" 时为 falsy,words[i] && ... 可安全跳过。
当用户要求为 Java 代码添加注释、修改注释、审查注释、整理注释格式,或要求按照代码注释规范处理 Java 文件时,使用本技能。本技能用于统一生成和维护 Java 类注释模板、按 Git 对比 release 判断版本号、补充每行代码上方中文块注释、调整注释空行规则,并保持 Java 文件 UTF-8 无 BOM 编码。典型触发场景包括:补充类注释、更新 @version、检查 @author 来源、整理行尾注释、把注释放到代码上一行、检查注释是否符合规范、构建前检查 Java 注释风格。
立即学习“Java免费学习笔记(深入)”;
? 完整健壮实现(含结构保护)
以下为优化后的生产就绪版本,已集成空字符串过滤、HTML 标签完整性保障及事件委托优化:
document.addEventListener("DOMContentLoaded", function() {
function shortenText(element, limit) {
const originalContent = element.innerHTML;
const nodes = Array.from(element.childNodes);
let wordCount = 0;
let truncatedContent = '';
function traverseNodes(node) {
if (wordCount >= limit) return;
if (node.nodeType === Node.TEXT_NODE) {
const text = node.nodeValue.trim();
if (!text) return; // 忽略纯空白文本节点
const words = text.split(/\s+/).filter(w => w.length > 0); // 双重过滤:trim + filter
for (let i = 0; i < words.length; i++) {
if (words[i] && wordCount < limit) {
truncatedContent += words[i] + ' ';
wordCount++;
} else if (words[i]) { // 超限时追加省略号(且该词本身非空)
truncatedContent += '... ';
return;
}
}
}
else if (node.nodeType === Node.ELEMENT_NODE) {
// 跳过已存在的 .more 元素,避免重复嵌套
if (node.nodeName.toLowerCase() === 'span' && node.classList.contains('more')) {
return;
}
// 安全重建标签(含属性)
const tagName = node.nodeName.toLowerCase();
truncatedContent += `<${tagName}`;
node.attributes.forEach(attr => {
// 对属性值做基础转义(防 XSS 基础防护)
const escapedValue = attr.value.replace(/"/g, '"');
truncatedContent += ` ${attr.name}="${escapedValue}"`;
});
truncatedContent += '>';
// 递归处理子节点
node.childNodes.forEach(traverseNodes);
truncatedContent += `</${tagName}>`;
}
}
// 逐节点遍历,确保顺序与 DOM 一致
nodes.forEach(child => {
if (wordCount < limit) traverseNodes(child);
});
// 拼接截断后内容 + 展开按钮
element.innerHTML = truncatedContent.trim() + '<span class="more">Read more</span>';
element.dataset.originalContent = originalContent;
}
function expandText(element) {
element.innerHTML = element.dataset.originalContent;
}
// 使用事件委托优化性能(避免每个 .more 绑定独立 listener)
document.addEventListener('click', function(e) {
const moreEl = e.target.closest('.shorten-text .more');
if (moreEl) {
const container = moreEl.closest('.shorten-text');
if (container) expandText(container);
}
});
// 初始化所有截断容器
document.querySelectorAll('.shorten-text').forEach(el => {
const limit = parseInt(el.dataset.limit, 10) || 100;
shortenText(el, limit);
});
});⚠️ 注意事项与最佳实践
-
不要依赖
innerHTML做原始内容存储:若内容含用户输入,需服务端净化;前端可改用dataset存储序列化文本(如JSON.stringify({html: ...})),但本例为简化保留innerHTML。 -
CSS 兼容性:
.more使用cursor: pointer和text-decoration: underline已满足基本可访问性,建议补充role="button"和键盘支持(如Enter触发)以符合 WCAG。 -
性能提示:对长文档频繁调用
shortenText时,可缓存originalContent并使用DocumentFragment批量操作 DOM,减少重排。 -
扩展建议:如需支持「按字符截断」或「保留首段完整」等策略,可将
traverseNodes抽离为可配置策略函数。
通过严格过滤空字符串、保留标签嵌套结构、并采用事件委托优化交互,该方案可稳定支撑多层级 HTML 内容的词数截断需求,彻底解决“截断过早”与“破坏 DOM 结构”的典型问题。

















