前端插件-不固定高度的DIV如何增加transition

这篇文章主要讲怎么给不固定高度的div增加过渡效果,涉及前端开发领域

需要使用者具备下面的开发环境

  1. typescript
  2. vue3
  3. 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

什么是ResizeObserverResuseObserver 是一个现代的浏览器 API,它用于监听元素尺寸的变化 。你可以把它想象成专门针对元素大小(widthheight)的 MutationObserverIntersectionObserver

在它出现之前,要监听一个元素的大小变化非常麻烦且低效,通常需要依赖监听 windowresize 事件,然后通过循环或复杂的方式去检查目标元素的大小是否改变了。ResizeObserver 提供了一个高效、专一且性能良好的解决方案。

为什么要用ResizeObserver:

  1. 响应式设计: 当元素的尺寸因CSS媒体查询、容器查询(Container Queries)、Flexbox或Grid布局的重新计算、内容增减、甚至是窗口缩放而改变时,我们需要执行一些操作(例如,重新绘制图表、调整地图大小、重新布局等)。
  2. 性能: 相比于在 window.resize 上绑定事件然后循环检查所有元素的古老方法,ResizeObserver 只在特定元素尺寸变化时才会触发回调,大大提升了性能。
  3. 替代有问题的 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栏进行切换时【节点数据/节点样式进行切换】

图三

实际的过渡效果如图四所示

图四

谢谢大家观看

相关推荐
却尘2 小时前
Server Actions 深度剖析(2):缓存管理与重新验证,如何用一行代码干掉整个客户端状态层
前端·客户端·next.js
小菜全2 小时前
Vue 3 + TypeScript 事件触发与数据绑定方法
前端·javascript·vue.js
Hilaku2 小时前
面试官开始问我AI了,前端的危机真的来了吗?
前端·javascript·面试
shellvon3 小时前
前端攻防:揭秘 Chrome DevTools 与反调试的博弈
前端·逆向
β添砖java3 小时前
案例二:登高千古第一绝句
前端·javascript·css
却尘3 小时前
Server Actions 深度剖析:这就是个披着 React 外衣的 RPC
前端·rpc·next.js
南雨北斗4 小时前
Vue 3 修饰符(Modifiers)
前端
会豪4 小时前
工业仿真(simulation)--前端(七)--消息栏
前端
Jinuss4 小时前
Vue3源码reactivity响应式篇之computed计算属性
前端·vue3