React TS中实现Div内长文本短版展示及展开收起功能
React长文本截断+展开收起组件解决方案
方法1:利用DOM元素的getClientRects()计算实际行数(最精准)
这个方法直接基于文本渲染后的实际行数据,能准确处理容器自动换行的情况:
- 先将完整文本渲染到目标容器,通过
getClientRects()获取每一行的矩形信息,集合的长度就是实际行数。 - 如果行数超过指定阈值,用二分法逐步截取文本,直到行数符合要求,同时拼接省略号;展开后切换为完整文本。
示例代码:
import { useState, useRef, useEffect } from 'react'; const TextTruncator = ({ text, maxLines = 3 }) => { const [isExpanded, setIsExpanded] = useState(false); const [truncatedText, setTruncatedText] = useState(text); const textRef = useRef(null); useEffect(() => { if (isExpanded || !textRef.current) return; const element = textRef.current; // 先渲染完整文本,获取真实行数 element.textContent = text; const lines = element.getClientRects(); if (lines.length <= maxLines) { setTruncatedText(text); return; } // 二分法查找最大可显示的文本长度 let low = 0; let high = text.length; let bestLength = text.length; while (low <= high) { const mid = Math.floor((low + high) / 2); element.textContent = text.slice(0, mid) + '...'; const currentLines = element.getClientRects(); if (currentLines.length <= maxLines) { bestLength = mid; low = mid + 1; } else { high = mid - 1; } } setTruncatedText(text.slice(0, bestLength) + '...'); }, [text, maxLines, isExpanded]); return ( <div ref={textRef} style={{ width: '300px' }}> {isExpanded ? text : truncatedText} {textRef.current && textRef.current.getClientRects().length > maxLines && ( <button onClick={() => setIsExpanded(!isExpanded)}> {isExpanded ? 'show less' : 'show more'} </button> )} </div> ); }; export default TextTruncator;
注意:要确保容器的样式(宽度、字体、行高)和最终显示完全一致,否则行数计算会出现偏差。
方法2:CSS line-clamp + JS控制(最简单)
如果不需要复杂的自定义截断逻辑,用CSS原生属性可以快速实现,JS只负责切换展开/收起状态:
import { useState } from 'react'; const TextTruncator = ({ text, maxLines = 3 }) => { const [isExpanded, setIsExpanded] = useState(false); return ( <div style={{ width: '300px' }}> <div style={{ display: '-webkit-box', WebkitLineClamp: isExpanded ? 'none' : maxLines, WebkitBoxOrient: 'vertical', overflow: 'hidden', textOverflow: 'ellipsis', }} > {text} </div> <button onClick={() => setIsExpanded(!isExpanded)}> {isExpanded ? 'show less' : 'show more'} </button> </div> ); }; export default TextTruncator;
优点:代码极简,无需手动处理文本截断;缺点:-webkit-line-clamp是webkit内核属性,虽然后续Firefox等主流浏览器已支持,但旧版浏览器可能需要降级方案。
方法3:Canvas计算文本宽度(适合自定义截断逻辑)
如果需要精细化控制(比如避免截断到单词中间),可以用Canvas计算文本宽度,逐词判断是否超出容器:
import { useState, useRef, useEffect } from 'react'; const TextTruncator = ({ text, maxLines = 3, containerWidth = 300, fontSize = '16px', lineHeight = '1.5' }) => { const [isExpanded, setIsExpanded] = useState(false); const [truncatedText, setTruncatedText] = useState(text); const canvasRef = useRef(document.createElement('canvas')); useEffect(() => { if (isExpanded) return; const ctx = canvasRef.current.getContext('2d'); ctx.font = `${fontSize} sans-serif`; const lineHeightPx = parseFloat(fontSize) * parseFloat(lineHeight); const maxHeight = lineHeightPx * maxLines; let currentHeight = 0; let currentLine = ''; let result = ''; // 按空格拆分单词,避免截断单词 const words = text.split(' '); for (const word of words) { const testLine = currentLine + (currentLine ? ' ' : '') + word; const lineWidth = ctx.measureText(testLine).width; if (lineWidth > containerWidth) { currentHeight += lineHeightPx; if (currentHeight + lineHeightPx > maxHeight) { // 处理最后一行,添加省略号 const finalLine = currentLine + '...'; if (ctx.measureText(finalLine).width <= containerWidth) { result += finalLine; } else { // 连当前行都放不下,逐字符截断 let truncated = ''; for (const char of currentLine) { truncated += char; if (ctx.measureText(truncated + '...').width > containerWidth) { truncated = truncated.slice(0, -1); break; } } result += truncated + '...'; } break; } result += currentLine + ' '; currentLine = word; } else { currentLine = testLine; } } // 处理剩余未超出的行 if (currentHeight + lineHeightPx <= maxHeight) { result += currentLine; } else { result += currentLine.slice(0, -3) + '...'; } setTruncatedText(result); }, [text, maxLines, containerWidth, fontSize, lineHeight, isExpanded]); return ( <div style={{ width: `${containerWidth}px`, fontSize, lineHeight }}> {isExpanded ? text : truncatedText} {truncatedText.length < text.length && ( <button onClick={() => setIsExpanded(!isExpanded)}> {isExpanded ? 'show less' : 'show more'} </button> )} </div> ); }; export default TextTruncator;
这个方法适合需要精确控制截断位置的场景,但要保证Canvas的字体样式和实际显示一致,否则宽度计算会有误差。
内容的提问来源于stack exchange,提问作者Ofir Kariv
相关产品推荐
相关产品推荐

