React+TS中如何为dangerouslySetInnerHTML内容添加阅读更多按钮
React+TS实现HTML内容截断+阅读更多功能
不需要依赖无TS类型声明的第三方库,原生实现逻辑轻量、类型安全,还能避免直接截断字符串导致的HTML标签未闭合、页面样式错乱问题。
核心实现代码
import { useState, useMemo } from 'react'; interface TruncatedHtmlProps { htmlContent: string; limit?: number; readMoreText?: string; } const TruncatedHtml = ({ htmlContent, limit = 200, readMoreText = 'Lire la suite +' }: TruncatedHtmlProps) => { const [isExpanded, setIsExpanded] = useState(false); // 计算是否需要展示阅读更多按钮 const showReadMore = useMemo(() => { if (typeof window === 'undefined') return false; const tempDom = document.createElement('div'); tempDom.innerHTML = htmlContent; return (tempDom.textContent || '').length > limit; }, [htmlContent, limit]); // 处理截断后的内容,自动保留完整HTML标签 const displayContent = useMemo(() => { if (isExpanded || !showReadMore) return htmlContent; const tempDom = document.createElement('div'); tempDom.innerHTML = htmlContent; let currentTextLength = 0; // 递归遍历节点截取内容,保证标签完整 const processNode = (node: Node): Node | null => { // 文本节点:按长度截取 if (node.nodeType === Node.TEXT_NODE) { const nodeText = node.textContent || ''; if (currentTextLength + nodeText.length <= limit) { currentTextLength += nodeText.length; return node.cloneNode(true); } const remainLength = limit - currentTextLength; currentTextLength = limit; return document.createTextNode(nodeText.slice(0, remainLength) + '...'); } // 元素节点:递归处理子节点 if (node.nodeType === Node.ELEMENT_NODE) { const clonedEl = (node as Element).cloneNode(false) as Element; node.childNodes.forEach(child => { if (currentTextLength >= limit) return; const processedChild = processNode(child); if (processedChild) clonedEl.appendChild(processedChild); }); return clonedEl; } return null; }; const truncatedContainer = document.createElement('div'); tempDom.childNodes.forEach(node => { if (currentTextLength >= limit) return; const processed = processNode(node); if (processed) truncatedContainer.appendChild(processed); }); return truncatedContainer.innerHTML; }, [htmlContent, isExpanded, limit, showReadMore]); return ( <> <p dangerouslySetInnerHTML={{ __html: displayContent }} className="my-4" /> {showReadMore && !isExpanded && ( <button className="underline" onClick={() => setIsExpanded(true)} > {readMoreText} </button> )} </> ); }; export default TruncatedHtml;
使用方法
直接替换你原有代码即可,默认按200字符截断,按钮文案和你原来的保持一致:
<TruncatedHtml htmlContent={template.content} />
如果需要自定义截断长度、按钮文案,传入对应props即可:
<TruncatedHtml htmlContent={template.content} limit={150} readMoreText="阅读更多" />
实现说明
- 无任何第三方依赖,不需要额外安装类型声明包,TS类型完整
- 截断逻辑通过DOM节点遍历实现,不会出现标签截断导致的样式错乱问题
- 字符计数按纯文本长度计算,不会把HTML标签字符计入长度,符合日常阅读计数逻辑
- 已做SSR兼容判断,在Next.js等服务端渲染框架中也可正常使用
内容的提问来源于stack exchange,提问作者Johan
相关产品推荐
相关产品推荐

