【你不知道的ElementUI】被隐藏的神级组件:Popper和它的管家
阅读本文📖
你将:
- 认识两个
ElementUI
官方文档没有记载,但实际已经被内置的组件。 - 学会使用它们,并获得
demo
一份。 - 了解
ElementUI
弹出层组件的基本原理,以后面对要不要appendToBody
及相关问题成竹于胸。
『你不知道的ElementUI』系列
本系列将会深入 ElementUI
,为你提供一些只有 ElementUI
资深使用者甚至代码贡献者才会知道的『神级知识』。
它们无论是在日常工作使用中,或是在面试装哔时,都非常实用。
另外:本系列也适合 Element Plus
的用户观看,因为 Element
的核心原理没有大的变化。
一、ElementUI
里所有弹出层的两种模式
ElementUI
的弹出层(包括但不限于:dialog
, select
, popover
, date-picker
等)在元素定位上,都有两种实现方式,分别是:
- 方案一:
append-to-body
式。此模式下,弹出层会被放在<body>
元素上,通过position:fixed
定位,配合动态的top
和left
属性,完成弹出元素的定位。 - 方案二: 非
append-to-body
式。此模式下,弹出层通过position:absolute
定位,配合其父元素position:relative
来完成弹出元素的定位。
在大多数情况下, ElementUI
都是默认使用的 『方案一:append-to-body
式』。
原因很简单,因为『方案二: 非 append-to-body
式』 存在严重副作用,只有迫不得已的情况下才需要使用。
例如,当弹出层组件的父元素拥有 position: relative; overflow: auto
样式时,是否 append-to-body
可能直接影响组件的显示:
示例代码:https://github.com/zhangshichun/blog-vue2-demos/tree/master/src/views/about-append-to-body
为什么『非
append-to-body
模式』会导致这个问题,而『append-to-body
模式』则不会?这是一道css基础题,欢迎把你的理解打在评论里。
除了上述场景,『非 append-to-body
模式』还会在多种场景下导致类似的问题。
这也是为什么 ElementUI
会把所有弹出层设置为『append-to-body
模式』的根本原因。
二、ElementUI
是如何管理弹出层的z-index
的?
第一节已经聊到了,除非特别指定,ElementUI
都会自动使用 『append-to-body
模式』来进行弹出层的实现。
在这个前提下,不妨假象自己是 ElementUI
的开发人员,你会怎么实现以下效果:
- 应该如何保证
dialog
组件里,select
组件的弹出层高于dialog
的弹出层? - 应该如何保证
dialog-1
上再次弹出的dialog-2
的层级高于dialog-1
的层级?
ElementUI
项目组的人员在设计这块的时候,想出了一个简单却让人拍案叫绝的办法:只要让新出现的弹出层,永远比之前所有弹出层的层级要高,就不会有『新弹层』被『旧弹层』遮盖的事情发生。
假设一个场景:“弹窗-1”上点击某个按钮弹出“弹窗-2”,而“弹窗-2”上又有个“下拉框组件”,此时它们的 z-index
会是什么样的呢?
如上图所示,只要保证每次“弹出层”在弹出时,都在当前最大 z-index
的基础上 +1
,就可以保证没有弹出层会被无故挡住。
记住这个机制,它是 ElementUI
弹出层的核心实现机制。
那么,又是有谁来管理整个项目的 z-index
的呢?
当然是—— popup管家
啦~
三、认识:老管家(PopupManager)
当蝙蝠侠在哥谭市和形形色色的反派殊死搏斗时,他的老管家阿福总能帮他将后方安排得妥妥贴贴。
ElementUI
里,所有弹出层的背后,都有这样一位辛勤的老管家,它就是 PopupManager
。
PopupManager: 为弹出层提供获取实例、注册、注销 等各种能力,但其最重要的能力,是提供了 z-index
的层级管理能力。
如何使用它?
<template>
<div>
<el-button @click="onClick">增加</el-button>
z-index: {{ value }} </div>
</template>
<script>
import { PopupManager } from 'element-ui/src/utils/popup'
export default {
data() {
return {
value: 0
}
},
methods: {
onClick() {
// 没错,使用起来就是这么简单,只要一行
this.value = PopupManager.nextZIndex()
}
}
}
</script>
效果如下:
发现没,第一次 PopupManager.nextZIndex()
会返回2000,然后每次点击都会 +1
。
后续的 +1
很好理解,每次弹出层要获取到的最新可用 z-index
就是要比之前的那一层多 1
。
但是为什么一开始会是 2000
呢?
其实这是 ElementUI
内置的一个“弹出层 z-index
基数”,但它是可以进行修改的。
在安装 ElementUI
时,通过以下代码可以让内置的 2000
变成 3000
。
// 这可以让弹出层的 z-index 从 3000 开始递增
Vue.use(Element, { zIndex: 3000 });
认识老管家的作用,不仅仅是理解 ElementUI
的 z-index
管理机制那么简单,它在实际生产和项目中也可以非常有用。
示例代码:https://github.com/zhangshichun/blog-vue2-demos/tree/master/src/views/popup-manager
四、实战:一个更灵活的全屏组件
官方全屏API虽然好,但产品经理说官方不懂业务。
众所周知,浏览器是有官方的全屏API的:Element.requestFullscreen()
,它可以让一个元素立刻铺满视窗,并且置于所有元素之上。
虽然,这是个看起来很美的 API
,但架不住产品经理总有一些奇葩的要求:“这个页面支持全屏,然后它上面还要有一些弹窗,弹窗里面是一些复杂的表单…”
当时听到这我就麻了:
“官方全屏是设定层面的高于一切,那些
append-to-body
的弹窗,无论z-index
再高,也绝对不可能被显示出来。”
“而那些非
append-to-body
模式的弹出层,确实会在某些业务场景不符合要求。)”
左思右想,还得封装一个“符合 ElementUI
层级标准的全屏组件”。
思路很简单:
和浏览器官方API实现全屏的思路基本一致,但不同的地方在于:
- 官方全屏会默认置顶,z-index无限大
- 自己封装的全屏,z-index符合
PopupManager
管家的规范。
因此我写了一个简陋的二十几行的 demo
:
<template>
<div :class="{ 'my-full-screen': isFullScreen }" :style="{zIndex: currentZIndex}">
<slot></slot>
</div>
</template>
<script>
import { PopupManager } from 'element-ui/src/utils/popup'
export default {
data() {
return {
isFullScreen: false,
currentZIndex: null
}
},
methods: {
request() {
this.isFullScreen = true
this.currentZIndex = PopupManager.nextZIndex()
}
}
}
</script>
<style>
.my-full-screen {
position: fixed !important;
top: 0 !important;
left: 0 !important;
right: 0 !important;
bottom: 0 !important;
width: 100% !important;
height: 100% !important;
}
</style>
效果上和浏览器官方API进行了对比:
虽然效果上肯定比不了官方的直接全屏,自行封装的还必须配合手动 F11
,但完全印证了一个问题:
PopupManager 完全可以用于生产,解决实际问题。
示例代码:https://github.com/zhangshichun/blog-vue2-demos/tree/master/src/views/full-screen
五、认识:万能弹出组件(vue-popper)
虽然管家阿福很厉害,但蝙蝠侠不能骑着阿福去战斗。于是有了蝙蝠战车。
vue-popper
的就是这辆“蝙蝠战车”。一点也不夸张。
我们所能接触到的,ElementUI
中的大部分弹出层都是基于 vue-popper
组件来实现的。
简单罗列一下:select
、date-picker族
、级联cascader
、dropdown
、popover
、tooltip
…等等,这些组件都是基于 vue-popper
组件来实现弹出层的。
那么 vue-popper
要怎么使用呢?
通常来说,它的主要用法是 混入(mixins)
。使用起来三步走:
最典型的例子,代码太多我就不列了,可以看看 ElementUI dropdown-menu
里对它的具体使用。代码见:https://github.com/ElemeFE/element/blob/dev/packages/dropdown/src/dropdown-menu.vue
混入的例子太复杂,有没有不用混入,直接把它当做组件的用法呢?
当然有!
五、实战:完全自定义的弹出层
如何使用 vue-popper
编写一个简单的“自定义弹出层”的 demo
。
按照以下三步即可:
- 首先我们引入
vue-popper
,在模板中引用该组件,并定义一个弹出层元素,一个定位元素。
<template>
<!-- 定位元素 -->
<div class="my-picker">
<!-- vue-popper组件 -->
<Popper ref="popper" v-model="showPopper">
</Popper>
<!-- 弹出组件 -->
<div ref="fly-piece" v-show="showPopper" class="my-picker__popper">你看,我弹出来了</div>
</div>
</template>
<script>
// 引入vue-popper组件
import Popper from 'element-ui/src/utils/vue-popper';
export default {
components: {
Popper
},
data() {
return {
// 双向绑定,控制弹出层是否弹出
showPopper: false
},
},
}
</script>
- 给
vue-popper
实例指定弹出层和定位层。
mounted() {
this.$refs.popper.popperElm = this.$refs['fly-piece'];
this.$refs.popper.referenceElm = this.$el;
}
- 通过控制
vue-popper
的props.value
来控制是否弹出。
this.showPopper = !this.showPopper
就能实现如下效果:
是不是很简单?
有了这个组件,当你想实现一些自定义的复杂组件的时,是不是很实用?
示例代码:https://github.com/zhangshichun/blog-vue2-demos/tree/master/src/views/my-picker
总结
ok,让我们回忆一下,本文我们讲了哪些知识点:
ElementUI
弹出层的两种模式,以及它们的实现原理。ElementUI
如何管理弹出层的z-index
。PopupManager
的用法及实战。vue-popper
的用法及实战。
- 点赞
- 收藏
- 关注作者
评论(0)