使用vue指令解決文本的溢出提示的問題
在我們項(xiàng)目開發(fā)中,經(jīng)常會(huì)有 超長文本溢出提示 ,未溢出則不提示的場景。
筆者就遇到了比較復(fù)雜的場景,在一個(gè)表格中,總計(jì)約有 5000 個(gè)單元格,需要對(duì)每個(gè)單元格進(jìn)行這個(gè)需求的校驗(yàn),剛開始開發(fā)的時(shí)候也是用 v-if v-else el-tooltip 一把梭。導(dǎo)致運(yùn)行時(shí)存在大量的 <el-tooltip> 實(shí)例。這樣的操作,代碼重復(fù)性極高,而且不利于后期維護(hù),關(guān)鍵我們發(fā)現(xiàn)性能極差,一拉伸就卡頓。和組長溝通后,開發(fā)出了這個(gè)指令。
接下來 讓我們一步步用 vue指令 實(shí)現(xiàn)這個(gè)需求
本文涉及到的技術(shù)棧
- vue2
- element-ui
在線體驗(yàn)
彥祖?zhèn)?這個(gè) codesandbox 好像用不了了,clone 到本地跑一下吧~
報(bào)錯(cuò) Error in render: "TypeError: (0 , _vue.resolveComponent) is not a function" , 如果有知道怎么解決的彥祖,請(qǐng)?jiān)u論區(qū) 留言。
常規(guī)開發(fā)
如下圖所示, 兩個(gè)寬度為 100px 的 div, 第一個(gè)不需要提示,第二個(gè)則需要提示

<template>
<div class="parent">
<div>
天翼云
</div>
<el-tooltip content="天翼云,海量數(shù)據(jù)">
<div class="tooltip">
天翼云,海量數(shù)據(jù)
</div>
</el-tooltip>
</div>
</template>
<style lang="scss" scoped>
.parent{
margin:100px;
>div{
border:1px solid lightblue;
width:100px;
margin-bottom:20px;
&.tooltip{
overflow: hidden; //超出的文本隱藏
text-overflow: ellipsis; //溢出用省略號(hào)顯示
white-space: nowrap; // 默認(rèn)不換行;
}
}
}
</style>這樣的代碼比較復(fù)雜,而且復(fù)用性極低。如果在其他頁面也有類似的場景,我們就不得不做一個(gè) cv 戰(zhàn)士 了
指令開發(fā)
如何判斷是否溢出?
這也算是一個(gè)知識(shí)點(diǎn),首先我們需要判斷文本是否溢出了節(jié)點(diǎn)。后來在 element-ui/packages/table/src/table-body.js 上找到了這段代碼
const range = document.createRange();
range.setStart(cellChild, 0);
range.setEnd(cellChild, cellChild.childNodes.length);
const rangeWidth = range.getBoundingClientRect().width;
const padding = (parseInt(getStyle(cellChild, 'paddingLeft'), 10) || 0) +
(parseInt(getStyle(cellChild, 'paddingRight'), 10) || 0);
if ((rangeWidth + padding > cellChild.offsetWidth || cellChild.scrollWidth > cellChild.offsetWidth) && this.$refs.tooltip) {
// 省略了不重要的邏輯
}那我們就改造一下 直接拿來用吧
實(shí)現(xiàn)溢出指令
接下來,讓我們用這段代碼 實(shí)現(xiàn)一個(gè)判斷溢出的指令,非常簡單,直接上代碼
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
// 避免用戶遺漏樣式,我們必須強(qiáng)制加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseEnter = (e) => {
if (isOverflow(el)) {
console.log('溢出了')
} else {
console.log('未溢出')
}
}
el.addEventListener('mouseenter', onMouseEnter)
}
}來看下效果吧

如何把溢出節(jié)點(diǎn)掛載到 el-tooltip 上?
本文的難點(diǎn)也是核心代碼,我們?cè)撊绾伟堰@個(gè)節(jié)點(diǎn)掛載到 <el-tooltip> 上呢。筆者也在這個(gè)問題上卡了非常久,嘗試過復(fù)制一個(gè)新節(jié)點(diǎn), 包裹一層 <el-tooltip> 去代替老的節(jié)點(diǎn),也嘗試過用 template 常規(guī)渲染調(diào)用,直接生成。但是發(fā)現(xiàn)都比較麻煩,最終不得去看了它的源碼。發(fā)現(xiàn)了以下代碼
mounted() {
this.referenceElm = this.$el;
if (this.$el.nodeType === 1) {
this.$el.setAttribute('aria-describedby', this.tooltipId);
this.$el.setAttribute('tabindex', this.tabindex);
on(this.referenceElm, 'mouseenter', this.show);
on(this.referenceElm, 'mouseleave', this.hide);
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus();
return;
}
const instance = this.$slots.default[0].componentInstance;
if (instance && instance.focus) {
instance.focus();
} else {
this.handleFocus();
}
});
on(this.referenceElm, 'blur', this.handleBlur);
on(this.referenceElm, 'click', this.removeFocusing);
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper();
}
});
}
}其實(shí)我們發(fā)現(xiàn) 核心點(diǎn) 就是這個(gè) this.$el , 它在 mounted 時(shí)對(duì) this.$el 進(jìn)行了一系列初始化操作
那么接下來就簡單了, 我們?cè)囍ヌ鎿Q這個(gè) this.$el ,然后再執(zhí)行 mounted 邏輯不就行了?
但是我們最好不要去改變這個(gè)屬性
vue 官方解釋

改造 el-tooltip 源碼
替換 el-tooltip 的 $el
我們只需要把源碼的 this.$el 部分改成 this.target
也就是在源碼內(nèi)部,我們新增一個(gè)
setEl(el){
this.target = el
}替換 mounted 邏輯
同樣非常簡單,我們把 mounted 的邏輯重新封裝一個(gè) init 方法
init () {
this.referenceElm = this.target
if (this.target.nodeType === 1) {
this.target.setAttribute('aria-describedby', this.tooltipId)
this.target.setAttribute('tabindex', this.tabindex)
on(this.referenceElm, 'mouseenter', this.show)
on(this.referenceElm, 'mouseleave', this.hide)
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus()
return
}
const instance = this.$slots.default[0].componentInstance
if (instance && instance.focus) {
instance.focus()
} else {
this.handleFocus()
}
})
on(this.referenceElm, 'blur', this.handleBlur)
on(this.referenceElm, 'click', this.removeFocusing)
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper()
}
})
}
}引入改造后的 el-tooltip
我們看下此時(shí)的目錄結(jié)構(gòu)

- directive.js //指令代碼
- main.js //改造過的 el-tooltip 代碼
這里還有個(gè)很重要的知識(shí)點(diǎn), 就是創(chuàng)建一個(gè) vue 實(shí)例
我們?cè)谌粘i_發(fā)中, 一般只會(huì)在 main.js 進(jìn)行一個(gè) new Vue 的操作。
在閱讀了 element-ui 源碼后,我們會(huì)發(fā)現(xiàn) el-message el-date-picker 中也用到了這個(gè)實(shí)例化的操作,更有 Vue.extend 等高階操作。
我們看下此時(shí)的 directive.js 代碼
import Vue from 'vue'
import Tooltip from './main'
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
// 加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseEnter = (e) => {
// 需要展示
if (isOverflow(el)) {
// 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount
const vm = new Vue(Tooltip).$mount()
vm.setEl(el)
vm.init()
}
}
el.addEventListener('mouseenter', onMouseEnter)
}
}看下此時(shí)的效果

已經(jīng)初步成效了,但是沒有任何顯示內(nèi)容?
填充顯示內(nèi)容
彥祖?zhèn)?這個(gè)可更簡單了 直接一個(gè) setContent 搞定了
- main.js
setContent(content){
this.content = content
}- directive.js
if (isOverflow(el)) {
// 參考 https://v2.cn.vuejs.org/v2/api/#vm-mount
const vm = new Vue(Tooltip).$mount()
vm.setEl(el)
vm.init()
vm.setContent('天翼云,海量數(shù)據(jù)')
}
此時(shí),可能有彥祖會(huì)說,這個(gè) setContent 也太復(fù)雜了,難道我每次都需要手動(dòng)傳入數(shù)據(jù)嗎?
當(dāng)然不必, 我們 默認(rèn) vm.setContent(content || el.innerText) 就好了
此處只做演示說明
此時(shí)我們基本已經(jīng)實(shí)現(xiàn)了一個(gè) 簡單的 ellipsis-tooltip
開始進(jìn)階
以下優(yōu)化代碼,只展示核心代碼,已去掉冗余代碼
防止重復(fù)實(shí)例化
如果我們不進(jìn)行 實(shí)例化的檢測,那么我們可能會(huì)存在大量的 vue 實(shí)例,用戶操作久了,就可能導(dǎo)致頁面卡頓
const vmMap = new WeakMap()
if (isOverflow(el)) {
if(vmMap.get(el)) return
const vm = new Vue(Tooltip).$mount()
// ...
vmMap.set(el,vm)
}兼容 el-tooltip 屬性
此時(shí),還有很多 el-tooltip 的源生屬性待支持的。比如 placement effect ...
其實(shí)我們只需要在代碼上加上
const vm = new Vue(Tooltip).$mount() vm.placement = placement || 'top-start' vm.effect = effect || 'dark' // ...其他屬性 不做贅述,自行拓展
兼容 文本 寬度改變的場景

業(yè)務(wù)中的文本寬度可能不是固定的
溢出的變成非溢出,非溢出也能變成溢出
所以我們需要在 mouseenter 的時(shí)候進(jìn)行對(duì)應(yīng)判斷
const vm = vmMap.get(el)
if (isOverflow(el)) {
if(vm) return vm.disabled = false
} else {
vm.disabled = true // 沒有溢出,則應(yīng)該禁用 tooltip
}
}移除大量實(shí)例化時(shí)候的節(jié)點(diǎn)
在開發(fā)中,筆者發(fā)現(xiàn), el-tooltip 在 render 的時(shí)候, 對(duì)應(yīng)的 dom 并不會(huì)從 文檔中移除,這個(gè)在表格或者樹這種大量節(jié)點(diǎn)的場景中, 性能開銷是我們不能接受的

我們開放一個(gè) destroyOnLeave 配置,用于設(shè)置 移出時(shí) 是否銷毀對(duì)應(yīng) 提示節(jié)點(diǎn)
const onMouseLeave = () => {
const elVm = vmMap.get(el)
if (!elVm) return
elVm.disabled = true
elVm.$nextTick(elVm.unMount) //卸載 tooltip,在 main.js 做了注釋
vmMap.set(el, null) // 銷毀內(nèi)存
}
if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)其他優(yōu)化項(xiàng)
其他一些常規(guī)的 eventListener 的移除操作,自我優(yōu)化,就不浪費(fèi)彥祖?zhèn)兊那啻毫?/p>
完整代碼
- main.js
import Popper from 'element-ui/src/utils/vue-popper'
import debounce from 'throttle-debounce/debounce'
import { addClass, removeClass, on, off } from 'element-ui/src/utils/dom'
import { generateId } from 'element-ui/src/utils/util'
import Vue from 'vue'
export default {
name: 'TooltipWrapper',
mixins: [Popper],
props: {
openDelay: {
type: Number,
default: 0
},
disabled: Boolean,
manual: Boolean,
effect: {
type: String,
default: 'dark'
},
arrowOffset: {
type: Number,
default: 0
},
popperClass: String,
content: String,
visibleArrow: {
default: true
},
transition: {
type: String,
default: 'el-fade-in-linear'
},
popperOptions: {
default () {
return {
boundariesPadding: 10,
gpuAcceleration: false
}
}
},
enterable: {
type: Boolean,
default: true
},
hideAfter: {
type: Number,
default: 0
},
tabindex: {
type: Number,
default: 0
}
},
data () {
return {
tooltipId: `el-tooltip-${generateId()}`,
timeoutPending: null,
focusing: false
}
},
beforeCreate () {
if (this.$isServer) return
this.popperVM = new Vue({
data: { node: '' },
render (h) {
return this.node
}
}).$mount()
this.debounceClose = debounce(200, () => this.handleClosePopper())
},
render (h) {
if (this.popperVM) {
this.popperVM.node = (
<transition
name={ this.transition }
onAfterLeave={ this.doDestroy }>
<div
onMouseleave={ () => { this.setExpectedState(false); this.debounceClose() } }
onMouseenter= { () => { this.setExpectedState(true) } }
ref="popper"
role="tooltip"
id={this.tooltipId}
aria-hidden={ (this.disabled || !this.showPopper) ? 'true' : 'false' }
v-show={!this.disabled && this.showPopper}
class={
['el-tooltip__popper', 'is-' + this.effect, this.popperClass]
}>
{ this.$slots.content || this.content }
</div>
</transition>)
}
const firstElement = this.getFirstElement()
if (!firstElement) return null
const data = firstElement.data = firstElement.data || {}
data.staticClass = this.addTooltipClass(data.staticClass)
return firstElement
},
watch: {
focusing (val) {
if (val) {
addClass(this.referenceElm, 'focusing')
} else {
removeClass(this.referenceElm, 'focusing')
}
}
},
methods: {
// 掛載目標(biāo)節(jié)點(diǎn)
setEl (el) {
this.target = el
},
setContent (content) {
this.content = content
},
init () {
this.referenceElm = this.target
if (this.target.nodeType === 1) {
this.target.setAttribute('aria-describedby', this.tooltipId)
this.target.setAttribute('tabindex', this.tabindex)
on(this.referenceElm, 'mouseenter', this.show)
on(this.referenceElm, 'mouseleave', this.hide)
on(this.referenceElm, 'focus', () => {
if (!this.$slots.default || !this.$slots.default.length) {
this.handleFocus()
return
}
const instance = this.$slots.default[0].componentInstance
if (instance && instance.focus) {
instance.focus()
} else {
this.handleFocus()
}
})
on(this.referenceElm, 'blur', this.handleBlur)
on(this.referenceElm, 'click', this.removeFocusing)
}
// fix issue https://github.com/ElemeFE/element/issues/14424
if (this.value && this.popperVM) {
this.popperVM.$nextTick(() => {
if (this.value) {
this.updatePopper()
}
})
}
},
show () {
this.setExpectedState(true)
this.handleShowPopper()
},
hide () {
this.setExpectedState(false)
this.debounceClose()
},
handleFocus () {
this.focusing = true
this.show()
},
handleBlur () {
this.focusing = false
this.hide()
},
removeFocusing () {
this.focusing = false
},
addTooltipClass (prev) {
if (!prev) {
return 'el-tooltip'
} else {
return 'el-tooltip ' + prev.replace('el-tooltip', '')
}
},
handleShowPopper () {
if (!this.expectedState || this.manual) return
clearTimeout(this.timeout)
this.timeout = setTimeout(() => {
this.showPopper = true
}, this.openDelay)
if (this.hideAfter > 0) {
this.timeoutPending = setTimeout(() => {
this.showPopper = false
}, this.hideAfter)
}
},
handleClosePopper () {
if (this.enterable && this.expectedState || this.manual) return
clearTimeout(this.timeout)
if (this.timeoutPending) {
clearTimeout(this.timeoutPending)
}
this.showPopper = false
if (this.disabled) {
this.doDestroy()
}
},
setExpectedState (expectedState) {
if (expectedState === false) {
clearTimeout(this.timeoutPending)
}
this.expectedState = expectedState
},
getFirstElement () {
if (this.slotEl) return this.slotEl
const slots = this.$slots.default
if (!Array.isArray(slots)) return null
let element = null
for (let index = 0; index < slots.length; index++) {
if (slots[index] && slots[index].tag) {
element = slots[index]
}
}
return element
},
unMount () {
if (this.popperVM) {
this.popperVM.$destroy() // 銷毀 popperVM 實(shí)例
this.popperVM.node && this.popperVM.node.elm.remove() // 移除對(duì)應(yīng)的 tooltip節(jié)點(diǎn)
}
const reference = this.referenceElm
// 解綁事件
if (reference.nodeType === 1) {
off(reference, 'mouseenter', this.show)
off(reference, 'mouseleave', this.hide)
off(reference, 'focus', this.handleFocus)
off(reference, 'blur', this.handleBlur)
off(reference, 'click', this.removeFocusing)
}
this.$nextTick(() => {
this.doDestroy() // 調(diào)用 mixins的 Popper.doDestroy 銷毀 popper
})
}
},
beforeDestroy () {
this.popperVM && this.popperVM.$destroy()
},
destroyed () {
const reference = this.referenceElm
if (reference.nodeType === 1) {
off(reference, 'mouseenter', this.show)
off(reference, 'mouseleave', this.hide)
off(reference, 'focus', this.handleFocus)
off(reference, 'blur', this.handleBlur)
off(reference, 'click', this.removeFocusing)
}
}
}- directive.js
import Vue from 'vue'
import Tooltip from './main'
const vmMap = new WeakMap()
const listenerMap = new WeakMap()
function getElStyleAttr (element, attr) {
const styles = window.getComputedStyle(element)
return styles[attr]
}
const isOverflow = (target) => {
const scrollWidth = target.scrollWidth
const offsetWidth = target.offsetWidth
const range = document.createRange()
range.setStart(target, 0)
range.setEnd(target, target.childNodes.length)
const rangeWidth = range.getBoundingClientRect().width
const padding = (parseInt(getElStyleAttr(target, 'paddingLeft'), 10) || 0) + (parseInt(getElStyleAttr(target, 'paddingRight'), 10) || 0)
return (rangeWidth + padding > target.offsetWidth) || scrollWidth > offsetWidth
}
export const ellipsisTooltip = {
bind (el, binding, vnode, oldVnode) {
const { value: { placement, disabled, content, effect, destroyOnLeave } = {} } = binding
if (disabled) return
// 加上超出...樣式
el.style.overflow = 'hidden'
el.style.textOverflow = 'ellipsis'
el.style.whiteSpace = 'nowrap'
const onMouseLeave = () => {
const elVm = vmMap.get(el)
if (!elVm) return
elVm.disabled = true
elVm.$nextTick(elVm.unMount)
vmMap.set(el, null)
}
const onMouseEnter = (e) => {
const elVm = vmMap.get(el)
// 需要展示
if (isOverflow(el)) {
if (elVm) {
elVm.disabled = false
return
}
const vm = new Vue(Tooltip).$mount()
vm.placement = placement || 'top-start'
vm.effect = effect || 'dark'
vm.setEl(el)
vm.setContent(content || el.innerText)
vm.init()
vm.show()
vmMap.set(el, vm)
if (destroyOnLeave) el.addEventListener('mouseleave', onMouseLeave)
} else {
if (elVm) elVm.disabled = true
}
}
listenerMap.set(el, [
['mouseenter', onMouseEnter]
]) // 用于拓展后續(xù)的監(jiān)聽
el.addEventListener('mouseenter', onMouseEnter)
},
unbind (el) {
const events = listenerMap.get(el)
if (events?.length) {
events.forEach(([name, event]) => el.removeEventListener(name, event))
}
}
}以上就是使用vue指令解決文本的溢出提示的問題的詳細(xì)內(nèi)容,更多關(guān)于vue指令解決文本溢出提示的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
利用Vue構(gòu)造器創(chuàng)建Form組件的通用解決方法
這篇文章主要給大家介紹了關(guān)于利用Vue構(gòu)造器創(chuàng)建Form組件的通用解決方法,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2018-12-12
vue前端實(shí)現(xiàn)導(dǎo)出頁面為word的兩種方法代碼
在前端開發(fā)中我們常常需要將頁面頁面為word文件,這篇文章主要給大家介紹了關(guān)于vue前端實(shí)現(xiàn)導(dǎo)出頁面為word的兩種方法,文中通過代碼介紹的非常詳細(xì),需要的朋友可以參考下2024-04-04
vue中this.$router.push()路由傳值和獲取的兩種常見方法匯總
這篇文章主要介紹了vue中this.$router.push()路由傳值和獲取的兩種常見方法,本文結(jié)合示例代碼給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2022-12-12
Vue項(xiàng)目打包部署到GitHub Pages的實(shí)現(xiàn)步驟
本文主要介紹了Vue項(xiàng)目打包部署到GitHub Pages的實(shí)現(xiàn)步驟,文中通過示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2022-04-04
vue項(xiàng)目中使用mapbox地圖切換底圖的詳細(xì)教程
最近開始入坑前端mapbox地圖,跟大家一起慢慢深入學(xué)習(xí),下面這篇文章主要給大家介紹了關(guān)于vue項(xiàng)目中使用mapbox地圖切換底圖的詳細(xì)教程,文中給出了詳細(xì)的實(shí)例代碼,需要的朋友可以參考下2023-04-04

