news 2026/5/21 17:50:26

ElementUI动画进阶:从零封装一个平滑的左右抽屉式折叠组件

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
ElementUI动画进阶:从零封装一个平滑的左右抽屉式折叠组件

1. 为什么需要左右抽屉式折叠组件

在后台管理系统开发中,"左树右表"的布局几乎成了标配设计。左侧是导航树或菜单栏,右侧是内容展示区。这种布局最大的痛点就是当左侧菜单项过多时,会挤压右侧内容的显示空间。这时候如果能像抽屉一样把左侧菜单收起来,用户体验会好很多。

ElementUI自带的折叠动画组件el-collapse-transition只能实现上下方向的折叠效果。但实际项目中,左右方向的折叠需求更为常见。比如:

  • 后台管理系统的侧边栏收缩
  • 电商平台商品筛选面板的隐藏/显示
  • 聊天应用的联系人列表折叠

我去年负责一个数据可视化平台项目时,就遇到了这个问题。产品经理要求左侧的指标选择面板要能平滑收起,给图表区更多展示空间。当时试了几种方案都不理想,最后决定基于ElementUI的动画原理自己封装一个。

2. 理解ElementUI的过渡动画原理

2.1 Vue的过渡系统基础

Vue提供了<transition>组件来实现元素进入/离开的过渡效果。它会在适当的时候添加/删除CSS类名,主要包括:

  • v-enter:进入动画的初始状态
  • v-enter-active:进入动画的激活状态
  • v-enter-to:进入动画的结束状态
  • v-leave:离开动画的初始状态
  • v-leave-active:离开动画的激活状态
  • v-leave-to:离开动画的结束状态

ElementUI的el-collapse-transition组件就是基于这套机制实现的。它通过JavaScript钩子函数精确控制过渡过程,比纯CSS方案更灵活。

2.2 分析el-collapse-transition源码

关键点在于它实现了6个JavaScript钩子:

  1. beforeEnter:进入动画开始前
  2. enter:进入动画执行中
  3. afterEnter:进入动画完成后
  4. beforeLeave:离开动画开始前
  5. leave:离开动画执行中
  6. afterLeave:离开动画完成后

这些钩子函数主要做了三件事:

  • 保存元素原始样式(如width、padding)
  • 应用过渡样式(如设置width为0)
  • 恢复原始样式

3. 实现左右折叠动画组件

3.1 组件基础结构

我们先创建一个名为HorizontalCollapseTransition.vue的文件:

<template> <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" > <slot></slot> </transition> </template> <script> export default { name: 'HorizontalCollapseTransition', methods: { // 这里实现各个动画钩子 } } </script>

3.2 核心动画钩子实现

重点在于width和padding的处理。下面是完整实现:

methods: { beforeEnter(el) { el.classList.add('horizontal-transition') if (!el.dataset) el.dataset = {} // 保存原始样式 el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight // 设置初始状态 el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' }, enter(el) { // 保存overflow状态 el.dataset.oldOverflow = el.style.overflow if (el.scrollWidth !== 0) { el.style.width = el.scrollWidth + 'px' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } else { el.style.width = '' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } el.style.overflow = 'hidden' }, afterEnter(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow }, beforeLeave(el) { if (!el.dataset) el.dataset = {} // 保存当前样式 el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.dataset.oldOverflow = el.style.overflow // 设置初始状态 el.style.width = el.scrollWidth + 'px' el.style.overflow = 'hidden' }, leave(el) { if (el.scrollWidth !== 0) { el.classList.add('horizontal-transition') el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' } }, afterLeave(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } }

3.3 添加过渡样式

在组件的<style>部分添加过渡效果:

.horizontal-transition { transition: width 0.3s ease-in-out, padding-left 0.3s ease-in-out, padding-right 0.3s ease-in-out; }

4. 在项目中使用组件

4.1 全局注册组件

在main.js中全局注册:

import HorizontalCollapseTransition from '@/components/HorizontalCollapseTransition' Vue.component('horizontal-collapse-transition', HorizontalCollapseTransition)

4.2 实现侧边栏折叠

典型的使用场景:

<template> <div class="layout"> <div class="sidebar" :style="{width: isCollapse ? '0' : '250px'}"> <horizontal-collapse-transition> <div class="menu-content" v-show="!isCollapse"> <!-- 菜单内容 --> </div> </horizontal-collapse-transition> </div> <div class="main-content"> <button @click="isCollapse = !isCollapse"> {{ isCollapse ? '展开' : '折叠' }} </button> <!-- 主内容区 --> </div> </div> </template> <script> export default { data() { return { isCollapse: false } } } </script> <style> .layout { display: flex; } .sidebar { overflow: hidden; } .menu-content { width: 250px; padding: 20px; } .main-content { flex: 1; } </style>

5. 进阶优化技巧

5.1 支持动态宽度

有时候折叠区域的宽度可能是动态的。我们可以通过props接收宽度值:

props: { width: { type: String, default: '250px' } }, methods: { enter(el) { // ...其他代码 if (el.scrollWidth !== 0) { el.style.width = this.width // 使用props传入的宽度 // ...其他代码 } } }

使用时:

<horizontal-collapse-transition width="300px"> <!-- 内容 --> </horizontal-collapse-transition>

5.2 性能优化建议

  1. 避免频繁重排:在动画过程中,浏览器需要不断重排。可以通过以下方式优化:

    • 对动画元素使用will-change: width
    • 确保动画元素不是复杂DOM结构的一部分
  2. 硬件加速:可以尝试添加transform: translateZ(0)来启用GPU加速

  3. 节流处理:快速连续点击折叠按钮时,可以添加防抖逻辑

data() { return { isCollapse: false, isAnimating: false } }, methods: { toggleCollapse() { if (this.isAnimating) return this.isAnimating = true this.isCollapse = !this.isCollapse setTimeout(() => { this.isAnimating = false }, 300) // 与动画持续时间一致 } }

5.3 与ElementUI组件集成

我们的组件可以完美配合ElementUI的菜单组件使用:

<el-menu :collapse="isCollapse" class="sidebar-menu" > <horizontal-collapse-transition> <div v-show="!isCollapse"> <el-menu-item index="1">首页</el-menu-item> <el-menu-item index="2">用户管理</el-menu-item> <el-menu-item index="3">订单管理</el-menu-item> </div> </horizontal-collapse-transition> </el-menu>

6. 常见问题与解决方案

6.1 动画卡顿问题

如果发现动画不够流畅,可以尝试以下解决方案:

  1. 检查CSS属性:确保没有在过渡期间改变会触发重排的属性,如height、margin等

  2. 简化DOM结构:动画元素内部的DOM结构越简单越好

  3. 使用requestAnimationFrame:修改enter和leave方法:

enter(el, done) { requestAnimationFrame(() => { // 动画逻辑 el.style.width = this.width el.addEventListener('transitionend', done) }) }

6.2 内容闪动问题

在组件初始渲染时,可能会出现内容闪动。解决方法:

  1. 确保初始状态一致:在mounted钩子中设置初始样式
mounted() { if (this.isCollapse) { this.$el.style.width = '0' this.$el.style.paddingLeft = '0' this.$el.style.paddingRight = '0' } }
  1. 使用v-if替代v-show:对于初始不可见的内容

6.3 浏览器兼容性问题

虽然现代浏览器都支持CSS过渡,但需要注意:

  1. IE兼容性:如果需要支持IE,可能需要添加polyfill

  2. 移动端适配:在移动设备上可能需要调整过渡时间

@media (max-width: 768px) { .horizontal-transition { transition-duration: 0.2s; } }

7. 完整组件代码

以下是经过优化的完整组件代码:

<template> <transition @before-enter="beforeEnter" @enter="enter" @after-enter="afterEnter" @before-leave="beforeLeave" @leave="leave" @after-leave="afterLeave" :css="false" > <slot></slot> </transition> </template> <script> export default { name: 'HorizontalCollapseTransition', props: { width: { type: String, default: '250px' }, duration: { type: Number, default: 300 } }, methods: { beforeEnter(el) { el.classList.add('horizontal-transition') if (!el.dataset) el.dataset = {} el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' }, enter(el, done) { requestAnimationFrame(() => { el.dataset.oldOverflow = el.style.overflow if (el.scrollWidth !== 0) { el.style.width = this.width el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } else { el.style.width = '' el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } el.style.overflow = 'hidden' setTimeout(done, this.duration) }) }, afterEnter(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow }, beforeLeave(el) { if (!el.dataset) el.dataset = {} el.dataset.oldPaddingLeft = el.style.paddingLeft el.dataset.oldPaddingRight = el.style.paddingRight el.dataset.oldOverflow = el.style.overflow el.style.width = el.scrollWidth + 'px' el.style.overflow = 'hidden' }, leave(el, done) { requestAnimationFrame(() => { if (el.scrollWidth !== 0) { el.classList.add('horizontal-transition') el.style.width = '0' el.style.paddingLeft = '0' el.style.paddingRight = '0' } setTimeout(done, this.duration) }) }, afterLeave(el) { el.classList.remove('horizontal-transition') el.style.width = '' el.style.overflow = el.dataset.oldOverflow el.style.paddingLeft = el.dataset.oldPaddingLeft el.style.paddingRight = el.dataset.oldPaddingRight } } } </script> <style> .horizontal-transition { transition-property: width, padding-left, padding-right; transition-timing-function: ease-in-out; will-change: width, padding-left, padding-right; } </style>

这个优化版本增加了props配置,使用了requestAnimationFrame提升性能,并添加了will-change优化提示。

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/5/17 4:39:14

5分钟掌握SQLite在线查看器:浏览器中的数据库管理革命

5分钟掌握SQLite在线查看器&#xff1a;浏览器中的数据库管理革命 【免费下载链接】sqlite-viewer View SQLite file online 项目地址: https://gitcode.com/gh_mirrors/sq/sqlite-viewer 在数据驱动的时代&#xff0c;SQLite数据库无处不在——从移动应用到嵌入式设备&…

作者头像 李华
网站建设 2026/4/1 19:25:11

手把手教你用DaVinci Configurator配置AUTOSAR接口(附实战避坑点)

手把手教你用DaVinci Configurator配置AUTOSAR接口&#xff08;附实战避坑点&#xff09; 在汽车电子控制单元&#xff08;ECU&#xff09;开发中&#xff0c;AUTOSAR架构已成为行业标准。作为Vector工具链的核心组件&#xff0c;DaVinci Configurator是工程师进行AUTOSAR软件…

作者头像 李华
网站建设 2026/5/19 1:20:15

抖音a_bogus逆向实战:手把手教你用Node.js补全缺失的window环境

抖音a_bogus逆向实战&#xff1a;Node.js环境补全指南 在JavaScript逆向工程领域&#xff0c;浏览器环境与服务端环境的差异一直是开发者面临的棘手问题。当我们尝试将抖音网页端的加密逻辑&#xff08;如a_bogus生成算法&#xff09;移植到Node.js环境时&#xff0c;经常会遇到…

作者头像 李华
网站建设 2026/4/1 19:24:08

千问3.5-2B应用场景:无障碍辅助——为视障用户实时描述手机相册图片

千问3.5-2B应用场景&#xff1a;无障碍辅助——为视障用户实时描述手机相册图片 1. 技术背景与价值 1.1 视障用户面临的数字鸿沟 在智能手机普及的今天&#xff0c;视觉障碍群体在使用手机相册时面临巨大挑战。他们无法像普通人一样通过视觉快速浏览照片内容&#xff0c;这导…

作者头像 李华
网站建设 2026/4/1 19:24:05

idea去除xml文件中SQL语句背景

1. 打开idea&#xff0c;依次点击File > Settings > Editor > Inspections > SQL2.在SQL下找到 No data sources configured 和 SQL dialect detection 取消勾选&#xff0c;记得Apply保存3.有些版本idea还需要以下步骤File > Settings > Editor > Color …

作者头像 李华
网站建设 2026/5/9 11:32:38

【C++PCL】点云处理总目录持续更新.....

作者&#xff1a;迅卓科技 简介&#xff1a;本人从事过多项点云项目&#xff0c;并且负责的项目均已得到好评&#xff01; 重点&#xff1a;每个模块都有参数如何调试的讲解&#xff0c;即调试某个参数对结果的影响是什么&#xff0c;大家有问题可以评论哈&#xff0c;如果文章…

作者头像 李华