el-drawer组件源码学习

el-Drawer

种一棵🌲最好的时间是十年前,其次是现在。

背景

在优化公司自定义抽屉组件的过程中,我遇到了一些关于抽屉组件功能实现的疑问。为了解决这些疑问,我开始了element-ui库中el-drawer组件源码的阅读,以下就是新手小白的浅薄理解。

主要困惑点

  1. 遮罩层实现机制 :如何通过popup混入实现遮罩层。

  2. 外部点击关闭功能:组件外点击如何触发组件关闭。

关键实现

1. 遮罩层

el-drawer组件通过混入popup外部文件来实现其功能。关键步骤包括:

  1. 初始化 : 在组件挂载到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); 
  	},
}
  1. 显示逻辑:监听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();
      }
    }
  },
  1. 核心实现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的实现后,我考虑直接引入了elementUIpopup/index.jspopup-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层。

modal相关源码参考

相关推荐
C语言魔术师9 分钟前
【小游戏篇】三子棋游戏
前端·算法·游戏
匹马夕阳1 小时前
Vue 3中导航守卫(Navigation Guard)结合Axios实现token认证机制
前端·javascript·vue.js
你熬夜了吗?1 小时前
日历热力图,月度数据可视化图表(日活跃图、格子图)vue组件
前端·vue.js·信息可视化
桂月二二7 小时前
探索前端开发中的 Web Vitals —— 提升用户体验的关键技术
前端·ux
hunter2062069 小时前
ubuntu向一个pc主机通过web发送数据,pc端通过工具直接查看收到的数据
linux·前端·ubuntu
qzhqbb9 小时前
web服务器 网站部署的架构
服务器·前端·架构
刻刻帝的海角9 小时前
CSS 颜色
前端·css
九酒9 小时前
从UI稿到代码优化,看Trae AI 编辑器如何帮助开发者提效
前端·trae
浪浪山小白兔10 小时前
HTML5 新表单属性详解
前端·html·html5
lee57610 小时前
npm run dev 时直接打开Chrome浏览器
前端·chrome·npm