【W(wǎng)eb技術(shù)】810- 超過(guò)N行如何折疊并顯示“...查看全部”?
?多行文本超過(guò)指定行數(shù)隱藏超出部分并顯示“...查看全部”是一個(gè)常遇到的需求,網(wǎng)上也有人實(shí)現(xiàn)過(guò)類(lèi)似的功能,不過(guò)還是想自己寫(xiě)寫(xiě)看,
?
于是就寫(xiě)了一個(gè)Vue的組件,本文簡(jiǎn)單介紹一下實(shí)現(xiàn)思路。
遇到這個(gè)需求的同學(xué)可以嘗試一下這個(gè)組件,支持npm安裝使用:
一、需求描述
長(zhǎng)度不定的一段文字,最多顯示n行(比如3行),不超過(guò)n行正常顯示;超過(guò)n行則在最后一行尾部顯示“展開(kāi)”或“查看全部”之類(lèi)的按鈕,點(diǎn)擊按鈕則展開(kāi)顯示全部?jī)?nèi)容,或者跳轉(zhuǎn)到其它頁(yè)面展示所有內(nèi)容。
預(yù)期效果如下:

二、實(shí)現(xiàn)原理
?純CSS很難完美實(shí)現(xiàn)這個(gè)功能,所以還得借助JS來(lái)實(shí)現(xiàn),實(shí)現(xiàn)思路大體相似,都是判斷內(nèi)容是否超過(guò)指定行數(shù),超過(guò)則截取字符串的前x個(gè)字符,然后然后和“...查看全部”拼接在一起,這里的x即截取長(zhǎng)度,需要?jiǎng)討B(tài)計(jì)算。
?
想通過(guò)上述方案實(shí)現(xiàn),有幾個(gè)問(wèn)題需要解決:
怎樣判斷文字是否超過(guò)指定行數(shù) 如何計(jì)算字符串截取長(zhǎng)度 動(dòng)態(tài)響應(yīng),包括響應(yīng)頁(yè)面布局變動(dòng)、字符串變化、指定行數(shù)變化等 下面具體研究一下這些問(wèn)題。
1. 怎樣判斷一段文字是否超過(guò)指定行數(shù)?
首先解決一個(gè)小問(wèn)題:如何計(jì)算指定行數(shù)的高度?我首先想到的是使用textarea的rows屬性,指定行數(shù),然后計(jì)算textarea撐起的高度。另一個(gè)方法是將行高的計(jì)算值與行數(shù)相乘,即得到指定行數(shù)的高度,這個(gè)辦法我沒(méi)嘗試過(guò),但是想必可行。
解決了指定行數(shù)高度的問(wèn)題,計(jì)算一段文字是否超過(guò)指定行數(shù)就很容易了。
我們可以將指定行數(shù)的textarea使用絕對(duì)定位absolute脫離文檔流,放到文字的下方,然后通過(guò)文本容器的底部與textarea的底部相比較,如果文本容器的底部更靠下,說(shuō)明超過(guò)指定行數(shù)。
這個(gè)判斷可以通過(guò)getBoundingClientRect接口獲取到兩個(gè)容器的位置、大小信息,然后比較位置信息中的bottom屬性即可。
可以這樣設(shè)計(jì)DOM結(jié)構(gòu):
?<div?class="ellipsis-container">
????<div?class="textarea-container">
??????<textarea?rows="3"?readonly?tabindex="-1"/>
????div>
???<--?showContent表示字符串截取部分?-->?
????{{?showContent?}}?
????...?查看更多
div>
?然后使用CSS控制textarea,使其脫離文檔流并且不能被看到以及被觸發(fā)鼠標(biāo)事件等(textarea標(biāo)簽中的readonly以及tabIndex屬性是必要的):
?
.ellipsis-container?{
??text-align:?left;
??position:?relative;
??line-height:?1.5;
??padding:?0?!important;
}
.textarea-container?{
??position:?absolute;
??left:?0;
??right:?0;
??pointer-events:?none;
??opacity:?0;
??z-index:?-1;
}
textarea?{
??vertical-align:?middle;
??padding:?0;
??resize:?none;
??overflow:?hidden;
??font-size:?inherit;
??line-height:?inherit;
??outline:?one;
??border:?none;
}
2.如何計(jì)算字符串截取長(zhǎng)度x——雙邊逼近法(二分思想)
?只要可以判斷一段文字是否超過(guò)指定行數(shù),那我們就可以動(dòng)態(tài)地嘗試截取字符串,直到找到合適的截?cái)嚅L(zhǎng)度x。
?
這個(gè)長(zhǎng)度滿(mǎn)足從x的位置截?cái)嘧址鞍氩糠?“...查看全部”等文字剛好不會(huì)超出指定行數(shù)N,但是多截取一個(gè)字,則會(huì)超出N行。
最直觀的想法就是直接遍歷,讓x從0開(kāi)始增長(zhǎng)到顯示文本總長(zhǎng)度,對(duì)于每個(gè)x值,都計(jì)算一次文字是否超過(guò)N行,沒(méi)超過(guò)則加繼續(xù)遍歷,超過(guò)則獲得了合適的長(zhǎng)度x - 1,跳出循環(huán)。當(dāng)然也可以讓x從文本總長(zhǎng)度遞減遍歷。
不過(guò)這里最大的問(wèn)題在于瀏覽器的回流和重繪。因?yàn)槲覀兠看谓厝∽址夹枰獮g覽器重新渲染出來(lái)才能得到是否超過(guò)N行,這過(guò)程中就觸發(fā)了瀏覽器的重繪或回流,每次循環(huán)都會(huì)觸發(fā)一次。
而對(duì)于正常的需求來(lái)說(shuō),假設(shè)N取值是3,那很可能每次計(jì)算會(huì)導(dǎo)致50次以上的重繪或回流,這中間消耗的性能還是非常大的,不小心可能就是幾十毫秒甚至上百毫秒。這個(gè)計(jì)算過(guò)程應(yīng)該在一個(gè)任務(wù)(即常說(shuō)的”宏任務(wù)“)中完成,否則計(jì)算過(guò)程中會(huì)出現(xiàn)顯示閃動(dòng)的”異常“情況,所以可以說(shuō)計(jì)算過(guò)程是阻塞的,因此計(jì)算的總時(shí)間一定要控制到非常低,即要減少計(jì)算的次數(shù)。
可以考慮使用"雙邊逼近法"(或稱(chēng)”二分法“)查找合適的截取長(zhǎng)度x,大大減少?lài)L試的次數(shù)。
第一次先以文本長(zhǎng)度為截取長(zhǎng)度,計(jì)算是否超過(guò)N行,沒(méi)超過(guò)則停止計(jì)算;超過(guò)則取1/2長(zhǎng)度進(jìn)行截取,如果此時(shí)沒(méi)超過(guò)N行,則在1/2長(zhǎng)度到文本長(zhǎng)度之間繼續(xù)二分查找,如果超過(guò)則在0到1/2文本長(zhǎng)度中繼續(xù)二分查找。
直到查找區(qū)間開(kāi)始值與結(jié)束值相差為1,則開(kāi)始值即為所求。具體實(shí)現(xiàn)可以看下文中的完整代碼。
3.監(jiān)聽(tīng)頁(yè)面變動(dòng)
對(duì)于Vue項(xiàng)目來(lái)說(shuō),傳入組件的字符串、行數(shù)等可能隨時(shí)改變,可以watch這些屬性變化,然后重新計(jì)算一次截取長(zhǎng)度。
另一方面,對(duì)于頁(yè)面布局而言,可能會(huì)因?yàn)槠渌?yè)面元素的增刪或者樣式改變,導(dǎo)致頁(yè)面布局變動(dòng),影響到文本容器的寬度,此時(shí)也應(yīng)該重新計(jì)算一次截取長(zhǎng)度。
監(jiān)聽(tīng)文本容器寬度的變化,可以考慮使用ResizeObserver來(lái)監(jiān)聽(tīng),但是這個(gè)接口的兼容性不夠好(IE各個(gè)版本都不支持),因此選擇了一個(gè)npm庫(kù)element-resize-detector來(lái)監(jiān)測(cè)(非常好用?)。
三、代碼實(shí)現(xiàn)
<template>
??<div?class="ellipsis-container">
????<div?class="textarea-container"?ref="shadow">
??????<textarea?:rows="rows"?readonly?tabindex="-1">textarea>
????div>
????{{?showContent?}}
????<slot?name="ellipsis"?v-if="(textLength?>
??????{{?ellipsisText?}}
??????<span?class="ellipsis-btn"?@click="clickBtn">{{?btnText?}}span>
????slot>
??div>
template>
import?resizeObserver?from?'element-resize-detector'
const?observer?=?resizeObserver()
export?default?{
??props:?{
????content:?{
??????type:?String,
??????default:?''
????},
????btnText:?{
??????type:?String,
??????default:?'展開(kāi)'
????},
????ellipsisText:?{
??????type:?String,
??????default:?'...'
????},
????rows:?{
??????type:?Number,
??????default:?6
????},
????btnShow:?{
??????type:?Boolean,
??????default:?false
????},
??},
??data?()?{
????return?{
??????textLength:?0,
??????beforeRefresh:?null
????}
??},
??computed:?{
????showContent?()?{
??????const?length?=?this.beforeRefresh???this.content.length?:?this.textLength
??????return?this.content.substr(0,?this.textLength)
????},
????watchData?()?{?//?用一個(gè)計(jì)算屬性來(lái)統(tǒng)一觀察需要關(guān)注的屬性變化
??????return?[this.content,?this.btnText,?this.ellipsisText,?this.rows,?this.btnShow]
????}
??},
??watch:?{
????watchData:?{
??????immediate:?true,
??????handler?()?{
????????this.refresh()
??????}
????},
??},
??mounted?()?{
????//?監(jiān)聽(tīng)尺寸變化
????observer.listenTo(this.$refs.shadow,?()?=>?this.refresh())
??},
??beforeDestroy?()?{
????observer.uninstall(this.$refs.shadow)
??},
??methods:?{
????refresh?()?{?//?計(jì)算截取長(zhǎng)度,存儲(chǔ)于textLength中
??????this.beforeRefresh?&&?this.beforeRefresh()
??????let?stopLoop?=?false
??????this.beforeRefresh?=?()?=>?stopLoop?=?true
??????this.textLength?=?this.content.length
??????const?checkLoop?=?(start,?end)?=>?{
????????if?(stopLoop?||?start?+?1?>=?end)?return
????????const?rect?=?this.$el.getBoundingClientRect()
????????const?shadowRect?=?this.$refs.shadow.getBoundingClientRect()
????????const?overflow?=?rect.bottom?>?shadowRect.bottom
????????overflow???(end?=?this.textLength)?:?(start?=?this.textLength)
????????this.textLength?=?Math.floor((start?+?end)?/?2)
????????this.$nextTick(()?=>?checkLoop(start,?end))
??????}
??????this.$nextTick(()?=>?checkLoop(0,?this.textLength))
????},
????//?展開(kāi)按鈕點(diǎn)擊事件向外部emit
????clickBtn?(event)?{
??????this.$emit('click-btn',?event)
????},
??}
}
在代碼實(shí)現(xiàn)中refresh函數(shù)用于計(jì)算截取長(zhǎng)度,在文本內(nèi)容、rows屬性等發(fā)生改變或者文本容器尺寸改變時(shí)將被調(diào)用。
每次refresh調(diào)用會(huì)異步地遞歸調(diào)用多次checkLoop,refresh可能重新調(diào)用,新的refresh調(diào)用將結(jié)束之前的checkLoop的調(diào)用。
四、其它
1. 支持HTML串的考慮
現(xiàn)在的實(shí)現(xiàn)方案并不支持內(nèi)容是HTML文本,如果需要支持HTML文本,問(wèn)題將復(fù)雜許多。主要在于HTML字符串的解析和截?cái)?,不像文本字字符串那么?jiǎn)單。
不過(guò)或許可以借助瀏覽器的Range API 來(lái)實(shí)現(xiàn)截?cái)辔恢玫亩ㄎ唬?code style>Range的insertNode以及setStart接口可以將“...查看全部”插入到指定位置,而如果插入位置剛好符合需要,則可以通過(guò)Range.cloneContents()接口取得截取HTML字符串的相關(guān)內(nèi)容,理論上是可行的,不過(guò)具體細(xì)節(jié)以及處理效率得實(shí)踐后才知道。
2. 減少瀏覽器回流的影響
上述實(shí)現(xiàn)方案中,每一次截取都需要瀏覽器重新渲染DOM,即重繪。
重繪的影響還比較小,而如果截取的字符串行數(shù)發(fā)生改變,還會(huì)引發(fā)文本容器的高度變化,這時(shí)候就會(huì)導(dǎo)致瀏覽器回流,而文本容器在文檔流中,回流將會(huì)影響整個(gè)文檔。
想解決這個(gè)問(wèn)題,可以使用一個(gè)脫離文檔流的元素來(lái)進(jìn)行字符串動(dòng)態(tài)截?cái)嗪蟮匿秩九c判斷,布局就類(lèi)似上述的textarea。
因?yàn)椴辉谖臋n流中,回流的影響范圍就會(huì)減少到該元素自身。獲得截?cái)嚅L(zhǎng)度后再截?cái)辔谋荆秩镜秸嬲奈谋救萜骷纯伞?/p>
本文僅作為一個(gè)簡(jiǎn)單的原理概述的示例,沒(méi)有做這個(gè)處理,對(duì)具體細(xì)節(jié)感興趣的同學(xué),可以查看github倉(cāng)庫(kù)代碼。
組件地址:
https://github.com/Lushenggang/vue-overflow-ellipsis
在線體驗(yàn):
https://wintc.top/laboratory/#/ellipsis
文章:
https://wintc.top/article/58

回復(fù)“加群”與大佬們一起交流學(xué)習(xí)~
點(diǎn)擊“閱讀原文”查看 80+ 篇原創(chuàng)文章
