el-Drawer
种一棵🌲最好的时间是十年前,其次是现在。
背景
在优化公司自定义抽屉组件的过程中,我遇到了一些关于抽屉组件功能实现的疑问。为了解决这些疑问,我开始了element-ui
库中el-drawer
组件源码的阅读,以下就是新手小白的浅薄理解。
主要困惑点
-
遮罩层实现机制 :如何通过
popup
混入实现遮罩层。 -
外部点击关闭功能:组件外点击如何触发组件关闭。
关键实现
1. 遮罩层
el-drawer
组件通过混入popup
外部文件来实现其功能。关键步骤包括:
- 初始化 : 在组件挂载到DOM之前,生成唯一的
_popupId
,并使用唯一的id注册一个新的popup
实例。
js
//element/src/utils/popup/index.<p align=left>js</p>
let idSeed = 1;
export default {
// // Registers the modal component with PopupManager when it's about to be mounted.
beforeMount() {
this._popupId = 'popup-' + idSeed++;
// Registers a new popup instance with a unique ID.
PopupManager.register(this._popupId, this);
},
}
- 显示逻辑:监听visible属性,控制遮罩层显示与隐藏。
我们可以看到在popup/index.js
中监听了visible
属性,visible
是控制Drawer组件显示或隐藏的关键变量。当抽屉打开时,会执行open()
方法。进而调用了doOpen
。
js
//element/src/utils/popup/index.js
watch: {
visible(val) {
if (val) {
if (this._opening) return;
if (!this.rendered) {
this.rendered = true;
Vue.nextTick(() => {
this.open();
});
} else {
this.open();
}
} else {
this.close();
}
}
},
- 核心实现 :
doOpen
方法中,根据modal属性决定是否显示遮罩层,并设置相应的zIndex
。
doOpen()
:是遮罩层实现的核心,它负责显示遮罩层。
js
//element/src/utils/popup/index.js
doOpen(props) {
if (this.$isServer) return // 方法检查当前环境是否为服务器端渲染,如果是,则直接返回并不执行后续操作
if (this.willOpen && !this.willOpen()) return // 检查 willOpen 钩子函数是否存在并且返回 false,如果存在并且返回 false,则不执行打开操作
if (this.opened) return // 检查遮罩层是否已经被打开,如果已经打开,则直接返回。
this._opening = true // 如果上述检查都通过,将 _opening 属性设置为 true,表示遮罩层正在打开。
const dom = this.$el; // 获取与遮罩层相关的 DOM 元素($el
const modal = props.modal; // 传入的是否显示遮罩层属性值
const zIndex = props.zIndex; //是否自定义了组件的zIndex值
if (zIndex) {
PopupManager.zIndex = zIndex;
}
// 显示遮罩层
if (modal) {
// 如果遮罩层正在关闭,则先关闭它
if (this._closing) {
PopupManager.closeModal(this._popupId)
this._closing = false
}
//使用 PopupManager 打开遮罩层,并设置其 zIndex 值。 modalAppendToBody: 默认为false
PopupManager.openModal(this._popupId, PopupManager.nextZIndex(), this.modalAppendToBody ? undefined : dom, props.modalClass, props.modalFade);
// 🔔 新手可以跳过这一段if (props.lockScroll) 代码块中的逻辑
// 如果设置了 lockScroll 属性,则处理滚动条的锁定。这可能包括在文档体(document.body)上添加一个类来隐藏滚动条,并调整文档体的 paddingRight 以防止内容溢出。
if (props.lockScroll) {
this.withoutHiddenClass = !hasClass(document.body, 'el-popup-parent--hidden')
if (this.withoutHiddenClass) {
this.bodyPaddingRight = document.body.style.paddingRight
this.computedBodyPaddingRight = parseInt(getStyle(document.body, 'paddingRight'), 10)
}
// scrollBarWidth = getScrollBarWidth()
const bodyHasOverflow = document.documentElement.clientHeight < document.body.scrollHeight
const bodyOverflowY = getStyle(document.body, 'overflowY')
if (scrollBarWidth > 0 && (bodyHasOverflow || bodyOverflowY === 'scroll') && this.withoutHiddenClass) {
document.body.style.paddingRight = this.computedBodyPaddingRight + scrollBarWidth + 'px'
}
addClass(document.body, 'el-popup-parent--hidden')
}
}
// 处理 position 和 zIndex
// 如果遮罩层的 DOM 元素的 position 样式属性为 static,则将其更改为 absolute,以便能够正确地进行定位和堆叠。
if (getComputedStyle(dom).position === 'static') {
dom.style.position = 'absolute'
}
dom.style.zIndex = PopupManager.nextZIndex() // 组件的层级要大于遮罩层的层级. 设置 DOM 元素的 zIndex 值,以确保它在其他元素之上显示。
this.opened = true // 将 opened 属性设置为 true,表示遮罩层已经打开
this.onOpen && this.onOpen() // 如果定义了 onOpen 方法,则调用它。这允许我们在遮罩层打开时执行自定义逻辑
this.doAfterOpen() // 调用 doAfterOpen 方法,这可能是一些在遮罩层打开后需要执行的额外逻辑
},
openModal()
打开modal,并将将其信息记录到 modalStack
数组中。
js
// element/src/utils/popup/popup-manager.js
/**
* @returns Returns the modal DOM element, creating it if it doesn't exist.
*/
const getModal = function() {
if (Vue.prototype.$isServer) return // 检查是否在 Vue 服务端渲染环境中,如果是,则直接返回,不执行后续操作
let modalDom = PopupManager.modalDom
if (modalDom) {
hasModal = true
} else {
hasModal = false
modalDom = document.createElement('div')
PopupManager.modalDom = modalDom
modalDom.addEventListener('touchmove', function(event) {
event.preventDefault()
event.stopPropagation()
})
modalDom.addEventListener('click', function() {
PopupManager.doOnModalClick && PopupManager.doOnModalClick()
})
}
return modalDom
}
const PopupManager = {
modalStack: [], // 跟踪当前打开的遮罩层的数组。 它维护有关每个遮罩层关联的组件的信息,例如其 ID、zIndex 和 modalClass.
/**
* @returns Returns the next available zIndex for a modal.
*/
nextZIndex: function() {
return PopupManager.zIndex++
},
// popupid,层级,dom,遮罩层class,遮罩层过渡动画(Boolean)默认为true
openModal: function(id, zIndex, dom, modalClass, modalFade) {
if (Vue.prototype.$isServer) return // 检查是否在 Vue 服务端渲染环境中,如果是,则直接返回,不执行后续操作。
if (!id || zIndex === undefined) return // 检查传入的参数 id 和 zIndex 是否存在,如果不存在,则直接返回,不执行后续操作。
const modalStack = this.modalStack;
// 遍历了 modalStack 数组,检查是否已经存在具有相同 id 的遮罩层,如果存在,则直接返回,不执行后续操作。
for (let i = 0, j = modalStack.length; i < j; i++) {
const item = modalStack[i];
if (item.id === id) {
return;
}
}
const modalDom = getModal(); // 获取modal DOM element
//遮罩层添加样式 过渡动画 移除动画
addClass(modalDom, 'v-modal');
if (this.modalFade && !hasModal) {
addClass(modalDom, 'v-modal-enter');
}
if (modalClass) {
let classArr = modalClass.trim().split(/\s+/);
classArr.forEach(item => addClass(modalDom, item));
}
setTimeout(() => {
removeClass(modalDom, 'v-modal-enter');
}, 200);
// 检查一个 DOM 元素(dom)的父节点是否不是一个文档片段(nodeType 为 11)。
if (dom && dom.parentNode && dom.parentNode.nodeType !== 11) {
dom.parentNode.appendChild(modalDom);
} else {
// 未指定父元素,则添加到 document.body 中。
document.body.appendChild(modalDom);
}
if (zIndex) {
modalDom.style.zIndex = zIndex;
}
//...
// 将modalDom的相关信息(id、zIndex、modalClass)添加到 modalStack 数组中。
this.modalStack.push({ id: id, zIndex: zIndex, modalClass: modalClass });
},
}
2. 点击Drawer组件范围外,关闭当前Drawer 的实现。
最开始研究公司项目自定义的抽屉组件时,发现原来的实现方式是给每一个抽屉组件在最外层包裹一个div 作为遮罩层,并通过addEventListener
监听点击事件来控制页面关闭。但是这样不是很优雅,于是开始了对element-ui
组件Drawer组件的modal实现的研究。
2.1 公司自定义抽屉组件的优化实现:
研究完Modal的实现后,我考虑直接引入了elementUI
中popup/index.js
和popup-manage.js
文件 ,开始尽可能避免去修改原有抽屉组件的代码,设想实现如下:
- 通过点击遮罩层时触发
popup-manage
中的click事件监听器,执行关闭modal的函数(doOnModalClick()
), - 然后在该函数中调用实例拥有的
close()
关闭自定义抽屉组件。同时在beforeDestory()
中触发PopupManager.closeModal(this._popupId)
来关闭modal遮罩层。
然后又回头去看了看element-ui
是如何实现的。
2.2 element-ui
的实现总结:
在抽屉模板设计时elementUI多包了一层div class="el-drawer__container"
给这个div添加了@click.self="handleWrapperClick"
。
由于抽屉实际上占满了全屏,所以其实不管是否展示modal层,点击抽屉组件范围外的地方实际上都是触发handleWrapperClick()
去关闭抽屉组件。
然后在visible值变成false
时,触发popup/index.js
中visible的监听回调方法,这个方法会依次执行close()
,this.doClose()
,this.doAfterClose()
,最后通过PopupManager.closeModal(this._popupId)
关闭modal层。