Element UI Drawer 抽屉组件完全指南:从基础用法到源码级原理剖析
2026/9/19 5:15:03 网站建设 项目流程

Element UI Drawer 抽屉组件完全指南:从基础用法到源码级原理剖析

【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element

当页面需要承载长表单、操作面板、条款协议或临时性的辅助内容时,传统的Dialog弹窗往往显得局促。Element UI 的Drawer(抽屉)组件提供了几乎与Dialog一致的 API,却带来了完全不同的交互体验:它从屏幕边缘滑入,不打断主流程,让复杂内容拥有更充足的空间。本文将以 fr-FR/drawer.md(及对应的 zh-CN/drawer.md)文档为主线,完整覆盖Drawer的全部配置项、插槽、方法与事件,并结合仓库源码剖析其底层实现原理,帮助你从"会用"进阶到"用得明白"。

Drawer 与 Dialog 的定位差异

Element UI 官方文档如此解释二者的关系:"Sometimes,Dialogdoes not always satisfy our requirements, let's say you have a massive form, or you need space to display something liketerms & conditions,Drawerhas almost identical API withDialog, but it introduces different user experience."——当你的表单很长,或者需要临时展示文档、条款等内容时,Drawer凭借侧滑展开的形态,在有限视口中开辟出独立的内容区域,体验上更接近"抽屉拉开、用完合上"的物理隐喻。

从实现层面看,Drawer组件同样复用了 Element UI 的Popup混入(见 src/utils/popup/index.js),与Dialog共享遮罩层、ESC 关闭、z-index 管理、滚动锁定等弹层基础设施,这是二者"几乎相同 API"的根本原因。而在视觉与动画上,Drawer则拥有独立的 SCSS 动画体系(见 packages/theme-chalk/src/drawer.scss),从四个方向滑入滑出。

基础用法:方向、尺寸与关闭前校验

Drawer的使用与Dialog如出一辙:通过visible属性(boolean类型,支持.sync修饰符)控制显隐。组件结构分为两部分——title(标题区,可通过具名插槽或title属性传入,默认空字符串)与body(内容区,即默认插槽)。

官方文档的基础示例完整展示了directionbefore-close两个核心 API 的配合使用:

<el-radio-group v-model="direction"> <el-radio label="ltr">left to right</el-radio> <el-radio label="rtl">right to left</el-radio> <el-radio label="ttb">top to bottom</el-radio> <el-radio label="btt">bottom to top</el-radio> </el-radio-group> <el-button @click="drawer = true" type="primary" style="margin-left: 16px;"> open </el-button> <el-drawer title="I am the title" :visible.sync="drawer" :direction="direction" :before-close="handleClose"> <span>Hi, there!</span> </el-drawer> <script> export default { data() { return { drawer: false, direction: 'rtl', }; }, methods: { handleClose(done) { this.$confirm('Are you sure you want to close this?') .then(_ => { done(); }) .catch(_ => {}); } } }; </script>

要点解读:

  • 默认方向rtl:Drawer 默认从右侧向左展开,宽度为浏览器窗口的30%。若需要其他方向,通过direction切换。
  • before-close拦截关闭:该回调接收一个done函数,只有当done()被调用时抽屉才会真正关闭;不调用则关闭流程被挂起。上述示例用$confirm弹窗做二次确认,正是"未保存内容防误关"的经典范式。

从源码看方向与尺寸的计算逻辑

在 packages/drawer/src/main.vue 中,方向与尺寸的实现非常直观:

  • 组件通过direction属性(validator校验仅接受ltr / rtl / ttb / btt四个值)与size属性(NumberString,默认'30%')驱动样式;
  • 计算属性isHorizontal判断是否为水平模式(rtl/ltr),进而决定size作用于宽度还是高度——水平模式作用于width,垂直模式作用于height
  • 计算属性drawerSize将数字类型的size转换为px单位,字符串则原样使用(文档明确:字符串请使用x%记法,否则会按像素解释)。

模板中的对应渲染语句为:

:style="isHorizontal ? `width: ${drawerSize}` : `height: ${drawerSize}`"

结合 drawer.scss 可见,水平方向(.ltr/.rtl)的抽屉占满高度,垂直方向(.ttb/.btt)的抽屉占满宽度,配合translate位移动画实现四个方向的滑入滑出。而 test/unit/specs/drawer.spec.js 中的size测试用例也验证了这一点:水平抽屉设置50%style.width'50%',垂直抽屉则反映在style.height上。

不显示标题:withHeader 与可访问性

当业务场景不需要标题栏时,可通过withHeader属性(默认true)将其关闭,从而为内容腾出更多空间:

<el-button @click="drawer = true" type="primary" style="margin-left: 16px;"> open </el-button> <el-drawer title="I am the title" :visible.sync="drawer" :with-header="false"> <span>Hi there!</span> </el-drawer> <script> export default { data() { return { drawer: false, }; } }; </script>

可访问性提醒:官方文档特别强调,隐藏 header 后务必仍设置title属性。查看源码模板可见,标题同时承担无障碍职责——.el-drawer根节点上的:aria-label="title"aria-labelledby="el-drawer__title"均与标题关联,屏幕阅读器依赖它来识别抽屉用途。此外,模板中v-if="withHeader"直接控制 header 区渲染,show-close(默认true)则控制右上角关闭按钮的出现与否,关闭按钮的:aria-label="close ${title || 'drawer'}"同样从标题生成。

自定义内容:嵌套表格与表单的完整实践

Dialog一样,Drawer内部可以承载任意复杂的业务内容。官方文档给出了两个极具代表性的场景:嵌套表格嵌套表单

嵌套表格的 Drawer

<el-button type="text" @click="table = true">Open Drawer with nested table</el-button> <el-drawer title="I have a nested table inside!" :visible.sync="table" direction="rtl" size="50%"> <el-table :data="gridData"> <el-table-column property="date" label="Date" width="150"></el-table-column> <el-table-column property="name" label="Name" width="200"></el-table-column> <el-table-column property="address" label="Address"></el-table-column> </el-table> </el-drawer>

这里将size放大到50%,让表格拥有充裕的展示宽度;gridData为表格数据源,示例中包含了date / name / address字段的多行数据。

嵌套表单的 Drawer 与 closeDrawer 方法

第二个抽屉嵌套了el-form表单,并演示了通过ref调用closeDrawer方法完成带提交状态的关闭流程:

<el-drawer title="I have a nested form inside!" :before-close="handleClose" :visible.sync="dialog" direction="ltr" custom-class="demo-drawer" ref="drawer" > <div class="demo-drawer__content"> <el-form :model="form"> <el-form-item label="Name" :label-width="formLabelWidth"> <el-input v-model="form.name" autocomplete="off"></el-input> </el-form-item> <el-form-item label="Area" :label-width="formLabelWidth"> <el-select v-model="form.region" placeholder="Please select activity area"> <el-option label="Area1" value="shanghai"></el-option> <el-option label="Area2" value="beijing"></el-option> </el-select> </el-form-item> </el-form> <div class="demo-drawer__footer"> <el-button @click="cancelForm">Cancel</el-button> <el-button type="primary" @click="$refs.drawer.closeDrawer()" :loading="loading">{{ loading ? 'Submitting ...' : 'Submit' }}</el-button> </div> </div> </el-drawer> <script> export default { data() { return { table: false, dialog: false, loading: false, gridData: [/* ...表格数据... */], form: { name: '', region: '', /* ...其他字段... */ }, formLabelWidth: '80px', timer: null, }; }, methods: { handleClose(done) { if (this.loading) { return; } this.$confirm('Do you want to submit?') .then(_ => { this.loading = true; this.timer = setTimeout(() => { done(); // animation takes time setTimeout(() => { this.loading = false; }, 400); }, 2000); }) .catch(_ => {}); }, cancelForm() { this.loading = false; this.dialog = false; clearTimeout(this.timer); } } } </script>

这一模式的关键在于:提交按钮直接调用$refs.drawer.closeDrawer(),而before-close回调负责在关闭前执行异步提交逻辑。源码中closeDrawer()的实现(packages/drawer/src/main.vue)正是如此:

closeDrawer() { if (typeof this.beforeClose === 'function') { this.beforeClose(this.hide); } else { this.hide(); } }

即:若设置了before-close,则把hide作为done传入并暂停关闭;否则直接关闭。hide(cancel)中,当cancel !== false时执行$emit('update:visible', false)$emit('close')——这也解释了为何传入的done必须被调用,关闭动作才得以继续。

多层嵌套 Drawer:append-to-body 是关键

Drawer同样支持多层嵌套,如同Dialog但官方文档特别强调:嵌套多层 Drawer 时,务必为内层 Drawer 设置append-to-bodytrue

<el-button @click="drawer = true" type="primary" style="margin-left: 16px;"> open </el-button> <el-drawer title="I'm outer Drawer" :visible.sync="drawer" size="50%"> <div> <el-button @click="innerDrawer = true">Click me!</el-button> <el-drawer title="I'm inner Drawer" :append-to-body="true" :before-close="handleClose" :visible.sync="innerDrawer"> <p>_(:зゝ∠)_</p> </el-drawer> </div> </el-drawer> <script> export default { data() { return { drawer: false, innerDrawer: false, }; }, methods: { handleClose(done) { this.$confirm('You still have unsaved data, proceed?') .then(_ => { done(); }) .catch(_ => {}); } } }; </script>

从源码看,appendToBody属性的作用在watch.visiblemounted两个生命周期中均有体现:当值为true时,将this.$el追加到document.body上(packages/drawer/src/main.vue),从而脱离外层 Drawer 的容器约束,避免内层抽屉被外层容器的overflow: hidden裁剪或遮挡。组件destroyed钩子中也会做对应的 DOM 清理。对应的单元测试(test/unit/specs/drawer.spec.js 中should append to body用例)验证了append-to-bodytrue时组件根节点直接挂载到document.body

三条关键实践提示(官方 Tip)

1. 内容懒渲染:DOM 操作请放在 open 事件后

Drawer 的内容是懒渲染的:在第一次打开之前,默认插槽内容不会被渲染到 DOM 上。因此任何 DOM 操作、或通过ref获取子组件实例,都应在open事件回调中进行。这与源码中rendered标志位(初始为false)及模板v-if="rendered"的条件渲染逻辑一致——只有首次打开后才真正挂载内容。

2. destroyOnClose:每次打开都重新挂载子组件

destroyOnClose(文档表格中写作destroy-on-close)是一个标志位,指示 Drawer 关闭后是否销毁内部子组件。当你需要每次打开都触发子组件的mounted生命周期(例如清理表单状态、保证初始数据一致性)时,将该属性设为true。源码中,无论是通过closeDrawer()关闭还是visible变化关闭,只要destroyOnClose === true,都会将rendered置回false,从而在下次打开时重新渲染。测试用例should destroy every child after drawer was closed when destroy-on-close flag is true验证了关闭 400ms 后.el-drawer__body被移除的行为。

3. Vuex 场景:去掉 .sync,监听 open / close 事件

如果visible绑定的变量存放在 Vuex store 中,.sync修饰符将无法正常工作(因为.sync需要组件就地更新父级变量,而 store 状态只能通过 mutation 修改)。此时请:

  • 去掉.sync修饰符;
  • 监听 Drawer 的openclose事件;
  • 在事件回调中调用 Vuex 对应的 mutation 来更新visible绑定的变量。

Drawer 完整 API 速查表

Attributes(属性)

参数说明类型可选值默认值
append-to-bodyDrawer 自身是否插入至 body 元素上,嵌套的 Drawer 必须设为 truebooleanfalse
before-close关闭前的回调,会暂停 Drawer 的关闭function(done),done 用于关闭 Drawer
close-on-press-escape是否可以通过按下 ESC 关闭 Drawerbooleantrue
custom-classDrawer 的自定义类名string
destroy-on-close控制是否在关闭 Drawer 后将子元素全部销毁booleanfalse
modal是否需要遮罩层booleantrue
modal-append-to-body遮罩层是否插入至 body 元素上,若为 false 则插入至 Drawer 的父元素booleantrue
directionDrawer 打开的方向Directionrtl / ltr / ttb / bttrtl
show-close是否显示关闭按钮booleantrue
sizeDrawer 窗体大小:number 类型以像素为单位;string 类型请传入 'x%',否则按像素解释number / string'30%'
titleDrawer 的标题,也可通过具名 slot 传入string
visible是否显示 Drawer,支持.sync修饰符booleanfalse
wrapperClosable点击遮罩层是否可以关闭 Drawerbooleantrue
withHeader是否显示 header 栏;为 false 时 title 属性与 title 插槽均不生效booleantrue

参数与源码 props 一一对应,可见 packages/drawer/src/main.vue 中的 props 定义;其中direction带 validator 校验,size支持Number / String双类型。

Slot(插槽)

name说明
Drawer 的内容(默认插槽)
titleDrawer 标题区的内容(具名插槽)

源码模板中,具名插槽title<slot name="title">包裹,未提供插槽时回退显示title属性的文本。

Methods(方法)

name说明
closeDrawer用于关闭 Drawer,该方法会调用传入的before-close方法

Events(事件)

事件名称说明回调参数
openDrawer 打开的回调(打开动画开始前触发)
openedDrawer 打开动画结束时的回调
closeDrawer 关闭的回调(关闭动画开始前触发)
closedDrawer 关闭动画结束时的回调

事件触发的底层依据:openwatch.visible置真时$emitopened/closed分别由<transition>@after-enter/@after-leave触发(对应模板中的afterEnter/afterLeave方法);closehide()watch.visible置假分支中$emit。在 test/unit/specs/drawer.spec.js 的events测试中,打开后等待 400ms 断言openopened被调用,关闭后等待 500ms 断言closeclosed被调用,完整验证了这套事件时序。

底层机制速览:Popup 混入与 ESC 关闭

Drawer通过mixins: [Popup, emitter]接入 Element UI 的弹层基础设施(src/utils/popup/index.js)。这意味着它自动获得:

  • 遮罩层管理modal属性控制遮罩层显隐,modalAppendToBody决定遮罩层插入位置,配合 src/utils/popup/popup-manager.js 中的 modalStack 栈式管理,保证多层弹层时遮罩层层级正确;
  • z-index 递增:每次打开调用PopupManager.nextZIndex(),确保后打开的抽屉覆盖先前的;
  • 滚动锁定:打开时给body添加el-popup-parent--hidden类并补偿滚动条宽度,关闭时恢复;
  • ESC 关闭popup-manager.jswindow上监听keydown,当event.keyCode === 27且栈顶实例closeOnPressEscape为真时调用其handleClose()。而 Drawer 的handleClose()内部转调closeDrawer()——这正解释了close-on-press-escape(默认true)为何同样会经过before-close校验。

值得一提的是,Drawer 的beforeClose逻辑对 ESC、遮罩点击、关闭按钮三条关闭路径一视同仁:handleWrapperClick(遮罩点击,受wrapperClosable控制)、关闭按钮@click="closeDrawer"、以及handleClose(ESC)最终都汇入closeDrawer()统一处理。

小结

Drawer是 Element UI 中与Dialog定位互补的弹层组件:四个方向的滑出动画、size/direction的灵活尺寸控制、before-close的关闭拦截、destroyOnClose的内容重置,以及基于 Popup 混入的弹层基础设施,使其成为承载长表单与复杂内容的理想容器。掌握append-to-body对多层嵌套的约束、理解.sync在 Vuex 场景下的限制,并将 DOM 操作收敛到open事件回调中,即可在实际项目中安全、高效地使用这一组件。

【免费下载链接】elementA Vue.js 2.0 UI Toolkit for Web项目地址: https://gitcode.com/gh_mirrors/eleme/element

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询