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(内容区,即默认插槽)。
官方文档的基础示例完整展示了direction、before-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属性(Number或String,默认'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-body为true:
<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.visible与mounted两个生命周期中均有体现:当值为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-body为true时组件根节点直接挂载到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 的
open与close事件; - 在事件回调中调用 Vuex 对应的 mutation 来更新
visible绑定的变量。
Drawer 完整 API 速查表
Attributes(属性)
| 参数 | 说明 | 类型 | 可选值 | 默认值 |
|---|---|---|---|---|
| append-to-body | Drawer 自身是否插入至 body 元素上,嵌套的 Drawer 必须设为 true | boolean | — | false |
| before-close | 关闭前的回调,会暂停 Drawer 的关闭 | function(done),done 用于关闭 Drawer | — | — |
| close-on-press-escape | 是否可以通过按下 ESC 关闭 Drawer | boolean | — | true |
| custom-class | Drawer 的自定义类名 | string | — | — |
| destroy-on-close | 控制是否在关闭 Drawer 后将子元素全部销毁 | boolean | — | false |
| modal | 是否需要遮罩层 | boolean | — | true |
| modal-append-to-body | 遮罩层是否插入至 body 元素上,若为 false 则插入至 Drawer 的父元素 | boolean | — | true |
| direction | Drawer 打开的方向 | Direction | rtl / ltr / ttb / btt | rtl |
| show-close | 是否显示关闭按钮 | boolean | — | true |
| size | Drawer 窗体大小:number 类型以像素为单位;string 类型请传入 'x%',否则按像素解释 | number / string | — | '30%' |
| title | Drawer 的标题,也可通过具名 slot 传入 | string | — | — |
| visible | 是否显示 Drawer,支持.sync修饰符 | boolean | — | false |
| wrapperClosable | 点击遮罩层是否可以关闭 Drawer | boolean | — | true |
| withHeader | 是否显示 header 栏;为 false 时 title 属性与 title 插槽均不生效 | boolean | — | true |
参数与源码 props 一一对应,可见 packages/drawer/src/main.vue 中的 props 定义;其中
direction带 validator 校验,size支持Number / String双类型。
Slot(插槽)
| name | 说明 |
|---|---|
| — | Drawer 的内容(默认插槽) |
| title | Drawer 标题区的内容(具名插槽) |
源码模板中,具名插槽title以<slot name="title">包裹,未提供插槽时回退显示title属性的文本。
Methods(方法)
| name | 说明 |
|---|---|
| closeDrawer | 用于关闭 Drawer,该方法会调用传入的before-close方法 |
Events(事件)
| 事件名称 | 说明 | 回调参数 |
|---|---|---|
| open | Drawer 打开的回调(打开动画开始前触发) | — |
| opened | Drawer 打开动画结束时的回调 | — |
| close | Drawer 关闭的回调(关闭动画开始前触发) | — |
| closed | Drawer 关闭动画结束时的回调 | — |
事件触发的底层依据:open在watch.visible置真时$emit;opened/closed分别由<transition>的@after-enter/@after-leave触发(对应模板中的afterEnter/afterLeave方法);close在hide()与watch.visible置假分支中$emit。在 test/unit/specs/drawer.spec.js 的events测试中,打开后等待 400ms 断言open、opened被调用,关闭后等待 500ms 断言close、closed被调用,完整验证了这套事件时序。
底层机制速览: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.js在window上监听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),仅供参考