ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

Vue组件实现多行文本展开收起:从line-clamp到scrollHeight溢出检测

Vue组件实现多行文本展开收起:从line-clamp到scrollHeight溢出检测 简介面向Vue开发者的多行文字展开收起实现示例聚焦长文本展示中“显示更多/收起”这一常见交互适合内容列表、文章摘要、详情介绍等前端场景。资源为PDF格式共1个文件仅36KB内容简明紧凑已有3510人学习与下载。文档以实际代码为主线详细讲解通过CSS的-webkit-line-clamp限制显示行数配合display:-webkit-box、-webkit-box-orient:vertical及overflow:hidden实现超长省略效果Vue部分则覆盖props文本传入、watch监听内容长度自动决定是否显示按钮、按钮click事件切换isDescStatus状态进而增减overflow-line类控制展开与收起同时动态更新按钮文案与箭头方向。包含完整的组件模板、Less scoped样式与可复用代码并演示了showmoreDesc方法的具体写法和 的调用方式。整体简单实用稍作调整即可适配不同行数帮助开发者少走弯路。1. 为什么展开收起不能只靠 CSS在很多后台管理系统和移动端资讯页里描述性文字动不动就是几十上百字直接铺开会让页面又长又碎。用 CSS 的-webkit-line-clamp可以把文本压成三行并显示省略号但纯 CSS 无法让用户“再看一点”。Vue 这里做的事情并不玄用一个布尔值控制描述段落是否保留overflow-line这个类同时把按钮的文案和箭头方向一起切换。如果你正在做 Vue 2 / Vue 3 项目需要快速实现多行文字的展开收起这篇文章给出的组件可以直接抄走也会说明为什么字符数判断不可靠、classList 切换有哪些坑。2. 先搞懂 -webkit-line-clamp 的约束再决定什么时候显示按钮2.1 为什么 -webkit-box 是 line-clamp 的前提-webkit-line-clamp是 WebKit 系的私有属性作用是把块容器内的文本限制在指定行数超出部分隐藏。它单独写出来并不会生效需要和另外几个声明配合使用.m-content { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis; }display: -webkit-box让元素进入老式弹性盒布局这是 line-clamp 能工作的基础-webkit-box-orient: vertical把主轴设成从上到下文本才会按列方向排列-webkit-line-clamp负责在第三行末尾截断并补上省略号overflow: hidden再把多出去的部分裁掉。text-overflow: ellipsis在多数浏览器里可以省略因为 line-clamp 自带省略号但某些 WebView 环境写上也无害。这个方案对 Chrome、Edge、Safari、Firefox 都没问题IE11 则不支持。如果项目必须兼容 IE11我一般会退回到max-height加渐变色遮罩的做法或者直接改成overflow: hidden三行截断不再纠结交互。下表是常见环境的支持程度环境-webkit-line-clamp 支持情况Chrome / Edge 80支持Safari / iOS WebView支持Firefox 68支持IE11不支持需要降级2.2 “是否溢出”的判断scrollHeight 比字符数可靠2.2.1 为什么 75 个字符不通用原始示例里用this.introduce.length 75决定是否显示“查看更多”。这个阈值在固定字号、固定容器宽度的列表页里可能碰巧成立但换成中英文混排、数字、URL 或者响应式布局就不准了。75 个英文和数字在同样宽度下可能只占不到一半75 个全角中文却会撑出三行半。更麻烦的是 emoji 在 JavaScript 的length里可能是 2显示时却是一个字符宽度。字符长度和视觉上的“行数”没有直接换算关系所以不能作为溢出依据。2.2.2 用 scrollHeight 检测的真实代码更可靠的做法是等 DOM 渲染完成后比较scrollHeight和clientHeight。scrollHeight是内容完整高度clientHeight是当前可见高度前者大于后者说明文本确实被裁掉了checkOverflow() { const el this.$refs.desc if (!el) return false return el.scrollHeight el.clientHeight 1 }代码里加1是为了容忍浏览器对高度做四舍五入带来的误差。clientHeight在.overflow-line生效时等于line-height乘以行数比如line-height: 22px且限制三行时clientHeight是 66px内容实际高度 67px说明第四行出现了按钮就应该显示。这个方法依赖$refs.desc已挂载所以调用时机要在nextTick或mounted之后。2.3 watch 初始化的坑immediate 与 nextTick默认的watch写法在组件初始化时不会执行回调。组件实例创建时 watcher 才注册父组件已经传进来的 prop 不会回头再触发一次。如果接口返回较慢mes2初始为空后面异步赋值时 watch 会触发看起来一切正常可一旦改成同步渲染或服务端直出按钮就会漏掉。修复方式是加immediate: true并在回调里用nextTick等待 DOM 更新watch: { mes2: { immediate: true, handler(val) { this.introduce val || this.isCollapsed true this.$nextTick(() { this.isShowMore this.checkOverflow() }) } } }这里重置isCollapsed是为了防止一种边界情况用户已经展开文本父组件随后推送了新的短文本。如果不重置isCollapsed一直是 falsep标签没有 line-clampscrollHeight会等于clientHeightisShowMore被改成 false按钮消失新文本就永远无法再收起。重置后再检测状态才能回到“先收起、再判断是否需要查看更多”的正确逻辑。3. 完整 Vue 组件模板、脚本、样式一次过3.1 template 结构p 标签、按钮与 ref 绑定模板部分用ref替代原始代码里的id。id在组件内多次复用时会产生重复ref只属于当前组件实例不会有全局冲突。按钮的显示由isShowMore控制文案由isCollapsed控制两者拆开避免展开后按钮因为isShowMore被误改而消失template div classexpand-wrap p refdesc classm-content :class{ overflow-line: isCollapsed } {{ introduce }}/p button v-ifisShowMore typebutton classbtn-more :class{ more-collapse: !isCollapsed } clicktoggle {{ isCollapsed ? 查看更多 : 收起 }}/button /div /template:class对象绑定比手写classList.add/remove更符合 Vue 的声明式写法。isCollapsed为 true 时p保留三行截断为 false 时移除.overflow-line文本完整展示。按钮上的more-collapse类用来控制箭头方向展开后箭头从向下变成向上。3.2 scriptprops、data、watch 与 toggle 方法script export default { name: SpreadText, props: { mes2: { type: String, default: } }, data() { return { introduce: , isCollapsed: true, isShowMore: false } }, watch: { mes2: { immediate: true, handler(val) { this.introduce val || this.isCollapsed true this.$nextTick(() { this.isShowMore this.checkOverflow() }) } } }, methods: { checkOverflow() { const el this.$refs.desc if (!el) return false return el.scrollHeight el.clientHeight 1 }, toggle() { this.isCollapsed !this.isCollapsed } } } /script这里保留了原始 props 名mes2实际项目中我更建议改成text或content语义更清楚。introduce是真正渲染到模板的字段避免直接修改 prop。toggle只负责反转isCollapsed剩余样式变化全靠 class 绑定完成方法越短越不容易出错。如果项目用的是 Vue 3 的script setup逻辑相同只是把props和watch的写法换成 Composition API核心状态仍然是isCollapsed、isShowMore、checkOverflow这三样。3.3 style省略号、按钮箭头和颜色变量原始 less 里用了rc()这个样式函数看起来是 rem 转换工具。没有这套工具时直接写px也能用。按钮箭头用两个伪元素叠放实现中间镂空的效果.expand-wrap { display: flex; flex-direction: column; align-items: flex-end; } .m-content { width: 100%; margin: 0; font-size: 14px; line-height: 22px; color: #333; .overflow-line { display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; word-break: break-all; } } .btn-more { position: relative; margin-top: 6px; padding-right: 24px; color: #5383E7; background: none; border: none; cursor: pointer; ::after, ::before { content: ; position: absolute; right: 0; top: 8px; width: 0; height: 0; border: 8px solid transparent; } ::after { border-top-color: #5383E7; z-index: 1; } ::before { border-top-color: #fff; z-index: 2; top: 6px; } .more-collapse::after, .more-collapse::before { transform: rotate(180deg); } }.expand-wrap用 flex 让按钮靠右下不依赖 float也省去清除浮动的麻烦。.m-content需要width: 100%否则 flex 容器会按内容宽度收缩导致溢出的判断结果不稳定。word-break: break-all主要对付长 URL没有这行的话连续英文可能把容器撑破省略号位置会异常。箭头部分::after是主题色::before是背景色且向上偏移 2px两个三角形叠在一起后中间出现一道缺口视觉上更像真实箭头。.more-collapse把两个伪元素同时旋转 180 度。这里有个前提页面背景必须是纯色否则::before的颜色会遮不住底图。如果页面是渐变或图片背景建议去掉::before只保留简单的三角形。4. 参数边界与容易翻车的四个细节4.1 classList 不要用三元表达式做方法名原始代码里有el.classList[!this.isDescStatus ? add : remove](overflow-line)。这行代码能跑通的原因是classList[add]确实能访问到方法但可读性很差ESLint 也很可能报no-eval-like-invocation之类的提示。方法名就应该当作方法调用不要用数组下标去取。常见做法是用classList.toggle的第二个参数强制添加或移除el.classList.toggle(overflow-line, !this.isCollapsed) el.classList.toggle(more-collapse, this.isCollapsed)第二个参数为 true 就添加false 就移除。不过在 Vue 组件内部我一般连这个都省掉直接通过:class绑定让框架去操作 DOM。另外一个明显的笔误是that.isShowMore true方法里根本没有定义that。在 Vue 2 的 methods 里this指向组件实例写that需要在外部const that this。这里应该是this.isShowMore true否则运行时会直接报错。4.2 展开后再次检测溢出的条件如果checkOverflow只在mounted里执行一次父组件后续更新文本时就不会重新判断。如果放在 watch 里又要注意展开状态的问题。已经展开的情况下p标签没有 line-clamp内容完整显示scrollHeight和clientHeight相等isShowMore会被改成 false按钮消失。所以我上面的 watch handler 第一件事就是this.isCollapsed true先把文本收回去再在nextTick里检测。这样每次文本变化都走一遍“收起 → 判断 → 决定按钮是否显示”的流程状态始终一致。也可以把文本变化单独放在一个方法里由父组件显式调用但 watch 对这种“外部 prop 驱动内部状态”的场景更省事。下面这张表总结了几个容易出问题的小场景场景错误现象根因建议初始化时 prop 已有值不显示“查看更多”watch 默认不 immediate加immediate: true展开后父组件更新文本按钮消失未重置收起状态就检测先重置isCollapsed用字符数 75 判断溢出中英文阈值不一致字符数与视觉行数无关用scrollHeight比较classList 用三元表达式做方法名可读性差且 lint 报错方法名误当属性访问用toggle或 class 绑定4.3 字体加载会影响高度计算clientHeight依赖line-height而line-height会被字体加载影响。项目如果使用 web font首次渲染时字体可能还没加载完浏览器先用 fallback 字体渲染此时计算出的高度是错的。加载完字体后再看原本三行能放下的文本可能变成三行多一点按钮始终不出现。处理方式是在字体加载完成后重新检测if (document.fonts document.fonts.ready) { document.fonts.ready.then(() { this.$nextTick(() { this.isShowMore this.checkOverflow() }) }) }同时在.m-content上显式设置line-height不要依赖normal。normal在不同浏览器和字体下换算结果不一样显式写死22px或24px能让溢出判断稳定很多。4.4 scoped 样式与 v-html 内容的兼容.overflow-line加在p标签上而文本是纯字符串时scoped 样式没有任何问题。如果introduce是富文本需要用v-html渲染-webkit-line-clamp依然作用于容器但内部的p、ul会自带 margin 和 padding可能导致scrollHeight比预期大。我一般会在容器里加一句:deep(*)清内外边距或者让渲染富文本的组件不要使用这个展开组件直接把富文本完整展示。5. 进阶动态行数、ResizeObserver 与验证方法5.1 把最大行数变成 props固定三行在多数场景够用但用户拿到组件后可能要两行、四行或者需要根据屏幕宽度切换。把maxLines抽成 props再通过内联样式绑定到-webkit-line-clamp即可p refdesc classm-content :class{ overflow-line: isCollapsed } :stylelineClampStyle {{ introduce }}/p脚本里加一个计算属性computed: { lineClampStyle() { return this.isCollapsed ? { -webkit-line-clamp: this.maxLines } : {} } }CSS 里保留display: -webkit-box、-webkit-box-orient: vertical、overflow: hidden这三项行数完全交给 style 控制。maxLines变化时clientHeight会跟着变所以还需要监听maxLines重新触发checkOverflowwatch: { maxLines() { this.$nextTick(() { this.isShowMore this.checkOverflow() }) } }5.2 用 ResizeObserver 监听容器尺寸变化响应式布局下侧边栏折叠、窗口宽度变化都会让文本容器宽度改变原本不溢出的内容可能突然溢出。ResizeObserver是比window.resize更准确的方案它观察组件根节点尺寸一变就重新计算mounted() { if (typeof ResizeObserver ! undefined) { this.resizeObserver new ResizeObserver(() { if (this.isCollapsed) { this.$nextTick(() { this.isShowMore this.checkOverflow() }) } }) this.resizeObserver.observe(this.$el) } }, beforeDestroy() { if (this.resizeObserver) { this.resizeObserver.disconnect() } }这里只在isCollapsed为 true 时检测避免展开状态下把isShowMore改成 false。组件销毁前必须disconnect否则 observer 会继续持有组件引用造成内存泄漏。5.3 验证方法手测边界与状态断言写完组件后我一般会按下面这套用例过一遍测试用例预期结果文本刚好三行不显示按钮无省略号文本三行加一个字显示“查看更多”第三行有省略号点击后按钮文案变为“收起”箭头向上父组件传入空字符串p 标签高度为 0按钮不显示容器宽度缩小一半ResizeObserver 触发之前不溢出的变成溢出自动化测试里可以挂载组件后直接断言wrapper.vm.isShowMore配合jest和vue/test-utils覆盖状态变化但省略号最终是否出现在第三行属于浏览器渲染行为单测看不到必须打开真实页面截图确认。把边界用例写进 CI 里的组件测试配合人工点一遍这个展开收起功能基本就稳了。本文还有配套的精品资源点击获取
返回列表