前言
实现文本溢出的展开收起功能,纯 css 方案在网页中可行,但在小程序中存在兼容性问题。
最优的解决方案就是使用 javascript 的二分截断法。
看了下 vant 的 textellipsis 组件源码。
理解了算法的实现原理后就写了一个uniapp版本和vue3版本的展开收起组件。
算法步骤:
- 创建隐藏容器并渲染内容。
- 计算最大行高(行数 × 单行行高)。
- 使用递归算法,类似于 tail(left, content.length)。
- 取中间值,并将其写入隐藏容器。
- 等待渲染完成后获取最新高度。
- 如果隐藏容器的高度超过最大行高,则继续调用 tail,使用 left = left,right = middle。
- 否则,可能是内容太少了(或者无法再继续截断,那就返回截取的内容)。使用 left = middle,right = right 继续调用 tail。
这个算法通过不断地二分截断,寻找到最合适的截取内容。
就算是1000多字,限定2行展示,截断次数也只在10次左右。
扩展:canvas海报的文字溢出功能也可以用这个算法。
uniapp版本
下面是从源码抽离出来单独封装的uniapp和vue3版本(网页,小程序,app都测试过)
先上效果图 300多ms:


uniapp版本有一些需要注意的点,如果兼容运行在小程序和app的话。
- 在小程序中,样式计算是在渲染过程中异步进行的,必须nexttick后才能获取容器最新高度(因为小程序样式计算是异步的。所以性能比不上网页的2ms,实测是300+ms)。
- 获取元素节点信息的方法也不一样。
- 行高如果是继承的获取的就是inherit。所以需要传行高进去。
<template>
<view
:class="{root:true,visible:!show}"
:style="{ lineheight: props.lineheight }"
>
{{ expanded ? props.content : text }}
<text class="action" v-if="hasaction" @click="onclickaction"
>{{ actiontext }}</text
>
</view>
<view :class="{hiddentext:true}" :style="{ lineheight: props.lineheight }"
>{{ text }}</view
>
</template>
<script lang="ts" setup>
import { defineprops, ref, getcurrentinstance, nexttick, computed, onmounted } from 'vue';
const instance = getcurrentinstance(); // 获取组件实例
const props = defineprops({
content: {
type: string,
default: ''
},
rows: {
type: number,
default: 2
},
lineheight: {
type: number,
default: '30rpx'
}
});
const expanded = ref(false);
const text = ref(props.content);
const hasaction = ref(false);
const show= ref(false);
const actiontext = computed(() => {
return expanded.value ? '收起' : '展开';
});
const onclickaction = () => {
expanded.value = !expanded.value;
};
// 查询元素形状信息
const qeuryrect = querytext => {
let query = uni.createselectorquery().in(instance);
return new promise((resolve, reject) => {
query
.select(querytext)
.boundingclientrect(rect => {
resolve(rect);
})
.exec();
});
};
// 查询元素样式属性等信息
const qeuryrectprop = querytext => {
let query = uni.createselectorquery().in(instance);
return new promise((resolve, reject) => {
query
.select(querytext)
.fields({ computedstyle: ['lineheight', 'height'], dataset: true, size: true }, rect => {
resolve(rect);
})
.exec();
});
};
let dots = '...';
let content = props.content;
let end = content.length;
const sethiddentext = val => {
return new promise((_, reject) => {
text.value = val;
console.error(val);
nexttick(() => {
_(val);
});
});
};
// 计算截断
const calcellipsistext = maxheight => {
const tail = async (left, right) => {
// 递归终止条件
if (right - left <= 1) {
return content.slice(0, left) + dots;
}
const middle = math.round((left + right) / 2);
// 设置拦截位置(注意slice 0,middle,虽然left ,right不断变,但是0是不变的)
await sethiddentext(content.slice(0, middle) + dots + actiontext.value);
let result = await qeuryrectprop('.hiddentext');
if (parseint(result.height) > maxheight) {
return tail(left, middle);
}
// 太往左了,内容不够,需要往右边移动
return tail(middle, right);
};
tail(0, end).then(res => {
text.value = res;
show.value=true
console.timeend("完成计算")
});
};
// 开始计算
onmounted(() => {
console.time("完成计算")
nexttick(async () => {
let result = await qeuryrectprop('.hiddentext');
let maxheight = parseint(result.lineheight) * props.rows;
// 隐藏的行高大于限定行数高度
if (maxheight < parseint(result.height)) {
hasaction.value = true;
calcellipsistext(maxheight);
} else {
hasaction.value = false;
text.value = props.content;
show.value=true
}
});
});
</script>
<style lang="scss" scoped>
.visible {
visibility: hidden;
}
.hiddentext {
position: fixed;
z-index: -999;
top: -9999px;
}
.action{
color:#1989fa;
}
</style>vue3版本
先上效果图:2ms

<template>
<div ref="root">
{{ expanded ? props.content : text }}
<span v-if="hasaction" class="action" @click="onclickaction">
{{ actiontext }}
</span>
</div>
</template>
<script setup>
import { ref, watch, computed, onmounted, onunmounted, onactivated, defineprops, defineemits } from 'vue'
const emit = defineemits(['clickaction'])
const props = defineprops({
rows: {
type: number,
default: 2,
},
dots: {
type: string,
default: '...',
},
content: {
type: string,
default: '',
},
expandtext: {
type: string,
default: '展开',
},
collapsetext: {
type: string,
default: '收起',
},
})
const usewindowresize = () => {
const window_width = ref(window.innerwidth)
onmounted(() => {
window.addeventlistener('resize', () => {
windowwidth.value = window.innerwidth
})
})
onunmounted(() => {
window.removeeventlistener('resize', () => {
windowwidth.value = window.innerwidth
})
})
return window_width
}
const windowwidth = usewindowresize()
const text = ref('')
const expanded = ref(false)
const hasaction = ref(false)
const root = ref(null)
let needrecalculate = false
const actiontext = computed(() => (expanded.value ? props.collapsetext : props.expandtext))
const pxtonum = (value) => {
if (!value) return 0
const match = value.match(/^\d*(\.\d*)?/)
return match ? number(match[0]) : 0
}
const clonecontainer = () => {
if (!root.value || !root.value.isconnected) return
const originstyle = window.getcomputedstyle(root.value)
const container = document.createelement('div')
const stylenames = array.from(originstyle)
stylenames.foreach((name) => {
container.style.setproperty(name, originstyle.getpropertyvalue(name))
})
container.style.position = 'fixed'
container.style.zindex = '-9999'
container.style.top = '-9999px'
container.style.height = 'auto'
container.style.minheight = 'auto'
container.style.maxheight = 'auto'
container.innertext = props.content
document.body.appendchild(container)
return container
}
const calcellipsised = () => {
console.time('完成计算')
const calcellipsistext = (container, maxheight) => {
const { content, dots } = props
const end = content.length
const calcellipse = () => {
const tail = (left, right) => {
// 递归终止条件
if (right - left <= 1) {
return content.slice(0, left) + dots
}
const middle = math.round((left + right) / 2)
// 设置拦截位置
container.innertext = content.slice(0, middle) + dots + actiontext.value
if (container.offsetheight > maxheight) {
return tail(left, middle)
}
// 太往左了,内容不够,需要往右边移动
return tail(middle, right)
}
container.innertext = tail(0, end)
console.timeend('完成计算')
}
calcellipse()
return container.innertext
}
// 计算截断文本
const container = clonecontainer()
if (!container) {
needrecalculate = true
return
}
const { paddingbottom, paddingtop, lineheight } = container.style
const maxheight = math.ceil(
(number(props.rows) + 0.5) * pxtonum(lineheight) + pxtonum(paddingtop) + pxtonum(paddingbottom)
)
if (maxheight < container.offsetheight) {
hasaction.value = true
text.value = calcellipsistext(container, maxheight)
} else {
hasaction.value = false
text.value = props.content
}
document.body.removechild(container)
}
const toggle = (isexpanded = !expanded.value) => {
expanded.value = isexpanded
}
const onclickaction = (event) => {
toggle()
emit('clickaction', event)
}
onmounted(calcellipsised)
onactivated(() => {
if (needrecalculate) {
needrecalculate = false
calcellipsised()
}
})
watch([windowwidth, () => [props.content, props.rows]], calcellipsised)
defineexpose({ toggle })
</script>
<style scoped>
.action {
color: #1989fa;
}
</style>
到此这篇关于使用javascript优雅实现文本展开收起功能的文章就介绍到这了,更多相关javascript文本展开收起内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论