『你不知道的ElementUI』被隐藏的神级组件:Popper和它的管家

9,746 阅读7分钟

阅读本文📖

你将:

  • 认识两个 ElementUI 官方文档没有记载,但实际已经被内置的组件。
  • 学会使用它们,并获得 demo 一份。
  • 了解 ElementUI 弹出层组件的基本原理,以后面对要不要 appendToBody 及相关问题成竹于胸。

『你不知道的ElementUI』系列

本系列将会深入 ElementUI,为你提供一些只有 ElementUI 资深使用者甚至代码贡献者才会知道的『神级知识』。
它们无论是在日常工作使用中,或是在面试装哔时,都非常实用。
另外:本系列也适合 Element Plus 的用户观看,因为 Element 的核心原理没有大的变化。

一、ElementUI 里所有弹出层的两种模式

ElementUI 的弹出层(包括但不限于:dialog, select, popover, date-picker 等)在元素定位上,都有两种实现方式,分别是:

  • 方案一: append-to-body 式。此模式下,弹出层会被放在 <body> 元素上,通过 position:fixed 定位,配合动态的 topleft 属性,完成弹出元素的定位。
  • 方案二: 非 append-to-body 式。此模式下,弹出层通过 position:absolute 定位,配合其父元素 position:relative 来完成弹出元素的定位。

在大多数情况下, ElementUI 都是默认使用的 『方案一:append-to-body 式』。
原因很简单,因为『方案二: 非 append-to-body 式』 存在严重副作用,只有迫不得已的情况下才需要使用。

例如,当弹出层组件的父元素拥有 position: relative; overflow: auto 样式时,是否 append-to-body 可能直接影响组件的显示:

非append-to-body模式的副作用

示例代码:github.com/zhangshichu…

为什么『非 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()

发现没,第一次 PopupManager.nextZIndex() 会返回2000,然后每次点击都会 +1

后续的 +1 很好理解,每次弹出层要获取到的最新可用 z-index 就是要比之前的那一层多 1

但是为什么一开始会是 2000 呢?

其实这是 ElementUI 内置的一个“弹出层 z-index 基数”,但它是可以进行修改的。

在安装 ElementUI 时,通过以下代码可以让内置的 2000 变成 3000

// 这可以让弹出层的 z-index 从 3000 开始递增
Vue.use(Element, { zIndex: 3000 });

认识老管家的作用,不仅仅是理解 ElementUIz-index 管理机制那么简单,它在实际生产和项目中也可以非常有用。

示例代码:github.com/zhangshichu…

四、实战:一个更灵活的全屏组件

官方全屏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 完全可以用于生产,解决实际问题。

示例代码:github.com/zhangshichu…

五、认识:万能弹出组件(vue-popper)

虽然管家阿福很厉害,但蝙蝠侠不能骑着阿福去战斗。于是有了蝙蝠战车。

vue-popper 的就是这辆“蝙蝠战车”。一点也不夸张。
我们所能接触到的,ElementUI 中的大部分弹出层都是基于 vue-popper 组件来实现的。

简单罗列一下:selectdate-picker族级联cascaderdropdownpopovertooltip...等等,这些组件都是基于 vue-popper 组件来实现弹出层的。

那么 vue-popper 要怎么使用呢?

通常来说,它的主要用法是 混入(mixins)。使用起来三步走:

vue-popper用法三步走

最典型的例子,代码太多我就不列了,可以看看 ElementUI dropdown-menu 里对它的具体使用。代码见:github.com/ElemeFE/ele…

混入的例子太复杂,有没有不用混入,直接把它当做组件的用法呢?

当然有!

五、实战:完全自定义的弹出层

如何使用 vue-popper 编写一个简单的“自定义弹出层”的 demo

按照以下三步即可:

  1. 首先我们引入 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>
  1. vue-popper 实例指定弹出层定位层
mounted() {
  this.$refs.popper.popperElm = this.$refs['fly-piece'];
  this.$refs.popper.referenceElm = this.$el;
}
  1. 通过控制 vue-popperprops.value 来控制是否弹出。
this.showPopper = !this.showPopper

就能实现如下效果: 简易弹出层

是不是很简单?

有了这个组件,当你想实现一些自定义的复杂组件的时,是不是很实用?

示例代码:github.com/zhangshichu…

总结

ok,让我们回忆一下,本文我们讲了哪些知识点:

  1. ElementUI 弹出层的两种模式,以及它们的实现原理。
  2. ElementUI 如何管理弹出层的 z-index
  3. PopupManager 的用法及实战。
  4. vue-popper 的用法及实战。