tldr: 如果你很熟悉 react context & react portals,这篇文章对于你的价值不大
📽 Background
历史原因
过时的
react
版本以及过时的antd
版本antd@v2.x
没有<Drawer />
组件
需求
行为与
antd@latest
中的<Drawer />
保持一致支持抽屉 push & pull 的 动态效果
抽屉 pull out 时,背景附加 暗化效果
支持 多层抽屉
子抽屉拉出的同时,父抽屉也需要 发生位移
示例:
🗿 Lame Idea
Step.01 组件布局 & 组件接口
按照 自顶向下
的设计思维,抽屉组件由三部分组成:
container: 用于承载整个抽屉组件
mask: 遮罩层,用与暗化抽屉外的背景区域,点击遮罩层可以关闭抽屉
content: 抽屉内部,又包括:
header
body
footer
content 部分平淡无奇,简单的 flex 布局即可搞定。重点放在 container 和 mask 的部分
有了布局之后,即可定义组件接口。这里给出接口的最小集:
visible
: boolean,控制抽屉的显示和隐藏onClose
: function,外部容器用于控制抽屉关闭的函数title
: string or obeject,抽屉标题栏的显示内容,可以是简单字符串或是复杂对象(下拉菜单等)footer
: object,抽屉底部区域,一般来说由若干个<Button />
组成
完成上述设计之后,写出以下代码:
几点说明:
line 30: 在多层抽屉嵌套的情况下,
push
用于判断 父抽屉 是否需要因为 子抽屉 而发生位移line 37: 抽屉打开时,
mask
遮罩层需要从opacity: 0
过渡到opacity: 0.3
,并添加一些别的 css 属性,后面会解释line 38: 抽屉打开时,用于给
content
部分添加特殊的 css 属性,后面会解释line 45: 抽屉关闭时,整个抽屉的
content
部分是隐藏在浏览器窗口的最右侧的。而container
和mask
部分是以 100% 的 width 和 height 直接覆盖在整个窗口之上的,并通过z-index
和pointer-events
这两个 css 属性来让用户感知不到他们的存在,后面会解释
Step.02 Css 细节处理
先长话短说,z-index
要很大,确保抽屉能在所有页面内容的最上层,且 container
和 mask
部分在抽屉隐藏的状态,不能影响页面其他内容的操作。
下面是具体做法:
z-index: 在没有很极限的特殊情况下,设置为 1000 即可
pointer-events
除了指示该元素不是鼠标事件的目标之外,值 none
表示鼠标事件 “穿透” 该元素并且指定该元素“下面”的任何东西。因此,给 container
部分添加 pointer-events: none;
之后,即使它拥有一个很大的 z-index
,抽屉外部的下层内容还是可以响应到用户的鼠标事件
回到 line: 37/38/45
,由于 mask
部分需要响应 onClose
事件,content
部分需要响应抽屉内部的所有鼠标事件,因此,在抽屉打开的状态下,需要给 mask
和 content
部分添加 pinter-events: auto;
下面给出样式代码,content
的样式名为 .drawer
, 其内部的部分省略
完成以上两步之后,一个基本的 单层 抽屉就做好了,很 simple 也很 basic
下面进行嵌套设计
Step.03 多层抽屉嵌套设计
多层嵌套的核心在于:子抽屉拉出时,父抽屉需要向左位移一定的距离。针对这一点,有以下几个问题
如何在 子抽屉 中控制 父抽屉 的行为?如果嵌套的层次很多,采用一堆的 callback 和 props 来控制显然是不合适的。实际上在 三层抽屉 的情况下,代码就已经很丑了
位移的是整个
container
部分还是只是content
部分?虽然乍一看,这两种做法在视觉效果上类似,但仔细一想,根据需求,mask
部分是一个 fade in & fade out 的效果,所以在一开始的组件布局设计中,container
和mask
部分是一开始就覆盖在页面之上的,抽屉打开时,仅仅只是把content
部分从右侧隐藏区域拉出来。因此,在子抽屉打开时,如果父抽屉位移的是content
部分,那在最后父抽屉关闭时,还需要把先前这段位移的距离再补回来,就不是简单的tranlateX(100%)
了。所以位移整个container
应该才是比较方便的做法多层抽屉的情况下,
<Drawer />
组件渲染到 dom 中应该是怎样的层级关系?举例来说,如果层级关系如下所示:
<div id='root'>
// ...
// 页面 A 中包含了一个多层嵌套的抽屉
<div>
// ...
// 第一层抽屉
<Drawer>
// 第二层抽屉
<Drawer>
</Drawer>
</Drawer>
</div>
</div>
根据第二个问题中,子抽屉打开时,父抽屉发生位移的是整个 container
部分,那么按照上面的层级关系,父抽屉的 container
实际上是包裹着子抽屉的,所以其实此时子抽屉也发生了不该发生的位移。因此,这样的设计显然是不合理的,替代为:
<div id='root'>
// ...
// 页面 A 中包含了一个多层嵌套的抽屉
<div>
// ...
</div>
</div>
// 第一层抽屉
<Drawer>
</Drawer>
// 第二层抽屉
<Drawer>
</Drawer>
如上面的代码所示,最终渲染到 dom 中之后,抽屉应当于 root 并列,但在书写代码的层面,<Drawer />
还是采用正常的嵌套逻辑写在 A 页面中,即在 virtual dom 中,<Drawer />
是嵌套的,在 真实 dom 中,<Drawer />
是独立且并列的
下面来解决这几个问题
💎 Smart Idea
Technically this part can be regard as an analysis of source code of antd-drawer and rc-drawer
React Context
回顾一下 Redux 的做法,Redux 通过 Context 机制实现了管理全局 state 的功能,使得任意位置的组件都能够很方便地获取到任何它想要的 props,这里不赘述
回到上面的第一个问题:如何在 子抽屉 中控制 父抽屉 的行为? 如果能在多层嵌套的抽屉中建立一个上下文,子抽屉 能够控制离它最近的 父抽屉 的行为,那么这个问题就解决了
Context 的定义:
Context provides a way to pass data through the component tree without having to pass props down manually at every level.
Context is primarily used when some data needs to be accessible by many components at different nesting levels.
具体的 API 和使用方法这里不展开了。这里得出的结论是:通过 Provider
和 Consumer
的形式,父抽屉 可以把自己本身(或者自己内部的某些属性和方法)传递给 子抽屉,这样 子抽屉 就可以去控制 父抽屉 的位移行为了。后面会给出具体代码
React Portals
Portals 的定义:
Portals provide a first-class way to render children into a DOM node that exists outside the DOM hierarchy of the parent component.
A typical use case for portals is when a parent component has an
overflow: hidden
orz-index
style, but you need the child to visually “break out” of its container. For example, dialogs, hovercards, and tooltips.
回到上面的第三个问题,实际上已经可以很明显地从 Portals
的定义中找到答案。同样给出结论:使用 ReactDOM.createPortal()
这个方法,把 <Drawer />
组件直接渲染到 document.body
中去
下面给出完整的 <Drawer />
代码
几点说明:
line 9:
DrawerContext
定义了嵌套抽屉的上下文,方法中传入 null 表示最顶层的抽屉没有父抽屉line 34:
parentDrawer
的值是通过 line 104 的DrawerContext.Provider
传递过来的line 37:
push
定义为 number 类型,可以理解为”被子抽屉 push 了几次“,并根据push
的值来渲染父抽屉的位移距离line 38, 41 ~ 45: 在
createPortal
的过程中,不能直接把<Drawer />
扔到document.body
中,要在外面包裹一层<div>
,然后再扔到body 中。因为<Drawer />
最外层的contaner
部分有z-index
属性,如果直接扔到 body 中,在多层嵌套的情况下会有问题(暂时没找到原因)line 47 ~ 62: 子抽屉打开时,控制父抽屉的位移行为
最终示例:
💡 Shallow Compnent, Deep Code
至此,一个扩展性还不错的多层嵌套抽屉就写好了。当然还可以做很多优化,比如添加更多可配置属性,控制抽屉的 destroy 行为等
想说的是,在刚开始写这个组件时,觉得是个很简单的普通组件,无非是控制一些动画效果而已。当然,通过一些简单粗暴的代码,也确实能够实现一开始提到的那些需求
但那样就没意思了