这篇文章主要讲怎么给不固定高度的div增加过渡效果,涉及前端开发领域
需要使用者具备下面的开发环境
- typescript
- vue3
- sass
那熟悉前端开发的同学都知道,想要给一个div增加过渡效果(transition),就必须给这个div设置一个高度值,如果高度值没有设置,那么transition属性就会失效
设计思路
由于我们是要做一个通用的组件,可以让别人开箱即用,那么就需要用到vue里面很重要的一个功能,插槽
1. 放置slot
首先在我们的组件内部放置一个slot,那么接下来就只需要拿到这个slot的一个高度,我的想法是这样的,在slot外部再包一个div,然后给这个div加上ref属性,当然这个div也是不固定高度,因为要随着slot高度变化而变化
html
<div :id="id" ref="contentRef">
<slot></slot>
</div>
那接下来我们就面临一个难题,怎么去监听slot高度,或者说监听contentRef的高度
2. ResizeObserver
什么是ResizeObserver :ResuseObserver
是一个现代的浏览器 API,它用于监听元素尺寸的变化 。你可以把它想象成专门针对元素大小(width
和 height
)的 MutationObserver
或 IntersectionObserver
。
在它出现之前,要监听一个元素的大小变化非常麻烦且低效,通常需要依赖监听 window
的 resize
事件,然后通过循环或复杂的方式去检查目标元素的大小是否改变了。ResizeObserver
提供了一个高效、专一且性能良好的解决方案。
为什么要用ResizeObserver:
- 响应式设计: 当元素的尺寸因CSS媒体查询、容器查询(Container Queries)、Flexbox或Grid布局的重新计算、内容增减、甚至是窗口缩放而改变时,我们需要执行一些操作(例如,重新绘制图表、调整地图大小、重新布局等)。
- 性能: 相比于在
window.resize
上绑定事件然后循环检查所有元素的古老方法,ResizeObserver
只在特定元素尺寸变化时才会触发回调,大大提升了性能。 - 替代有问题的
window.resize
:window.resize
事件会在用户每一次微调浏览器窗口时频繁触发,容易导致性能问题。而且它只能监听窗口变化,无法直接知道是哪个具体元素的大小变了。
然后在我们的的组件中就可以这样进行使用
ts
const contentRef = ref<HTMLElement | null>(null)
const state = ref<boolean>(props.initState)
const height = ref<undefined | string>(undefined)
let observer: ResizeObserver | null = null
// 初始化尺寸观察
const initResizeObserver = (): void => {
if (!contentRef.value) return
observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const newHeight = entry.contentRect.height
// 当处于展开状态时同步高度
if (state.value) {
height.value = `${newHeight + 3}px`
}
}
})
observer.observe(contentRef.value)
}
// 生命周期
onMounted(() => {
props.initState ? expand() : contract()
initResizeObserver()
})
当我们new了一个ResizeObserver实例时,就需要传入一个回调函数,这个函数就是当尺寸发生变化时,我们需要做的处理
而observe()方法就是监听的对象,那么我们可以看到,我是把contentRef.value对象传了进去,至于为什么不传入slot对象,这也是因为涉及到了一个很重要的开发思想,不要相信用户传入的东西
那么到此我们就已经实时获取contentRef的一个高度值,那接下来我们增加过渡效果
3. 增加过渡效果
若要增加过渡效果,就比不可能在contentRef上面加transition属性,因为会失效,所以我们就需要在contentRef外面再包一层div,在这个div增加transition属性
html
<template>
<div class="size-transition" :style="{ height }">
<div :id="id" ref="contentRef">
<slot></slot>
</div>
</div>
</template>
css
.size-transition {
width: 100%;
overflow: hidden;
transition: height 0.3s ease-in-out;
will-change: height; // 启用硬件加速
}
由于我们前面已经获取了height最新高度,所以我们可以直接将这个高度传入到style里面
到此我们的组件就设计完毕了
完整源代码
html
<template>
<div class="size-transition" :style="{ height }">
<div :id="id" ref="contentRef">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
import { generateUUID } from '@renderer/utils/utils'
import { ref, onMounted, onBeforeUnmount } from 'vue'
const props = defineProps({
minHeight: {
type: Number,
default: 0
},
initState: {
type: Boolean,
default: true
}
})
const id = ref<string>(generateUUID())
const contentRef = ref<HTMLElement | null>(null)
const state = ref<boolean>(props.initState)
const height = ref<undefined | string>(undefined)
let observer: ResizeObserver | null = null
// 初始化尺寸观察
const initResizeObserver = (): void => {
if (!contentRef.value) return
observer = new ResizeObserver((entries) => {
for (const entry of entries) {
const newHeight = entry.contentRect.height
// 当处于展开状态时同步高度
if (state.value) {
height.value = `${newHeight + 3}px`
}
}
})
observer.observe(contentRef.value)
}
// 折叠方法
const contract = (): void => {
if (!contentRef.value) return
height.value = `${contentRef.value.offsetHeight}px`
requestAnimationFrame(() => {
height.value = `${props.minHeight}px`
state.value = false
})
}
// 展开方法
const expand = (): void => {
if (!contentRef.value) return
height.value = '0px'
requestAnimationFrame(() => {
height.value = `${contentRef.value!.offsetHeight + 3}px`
state.value = true
})
}
// 切换方法
const zoom = (): void => {
state.value ? contract() : expand()
}
// 生命周期
onMounted(() => {
props.initState ? expand() : contract()
initResizeObserver()
})
onBeforeUnmount(() => {
observer?.disconnect()
})
defineExpose({
contract,
expand,
zoom
})
</script>
<style scoped lang="scss">
.size-transition {
width: 100%;
overflow: hidden;
transition: height 0.3s ease-in-out;
will-change: height; // 启用硬件加速
}
</style>
实际应用与效果
实际应用也很简单,只需要导入,然后将自己的div放入到里面即可
html
<template>
<SizeTransition>
<div>
可变高度
</div>
</SizeTransition>
</template>
import SizeTransition from '@renderer/components/sizeTransition/SizeTransition.vue'
接下来我们看看效果,大家看图一和图二,这是两个高度不同的div
图一
图二
当我们点击tab栏进行切换时【节点数据/节点样式进行切换】
图三
实际的过渡效果如图四所示
图四
谢谢大家观看