温馨提示×

温馨提示×

您好,登录后才能下订单哦!

密码登录×
登录注册×
其他方式登录
点击 登录注册 即表示同意《亿速云用户服务条款》

如何使用React Portals实现一个功能强大的抽屉组件

发布时间:2021-10-15 09:44:06 来源:亿速云 阅读:139 作者:iii 栏目:web开发

本篇内容介绍了“如何使用React Portals实现一个功能强大的抽屉组件”的有关知识,在实际案例的操作过程中,不少人都会遇到这样的困境,接下来就让小编带领大家学习一下如何处理这些情况吧!希望大家仔细阅读,能够学有所成!

正文

在开始组件设计之前希望大家对css3和js有一定的基础,并了解基本的react/vue语法.我们先看看实现后的组件效果:

如何使用React Portals实现一个功能强大的抽屉组件

1. 组件设计思路

按照之前笔者总结的组件设计原则,我们第一步是要确认需求. 一个抽屉(Drawer)组件会有如下需求点:

  • 能控制抽屉是否可见

  • 能手动配置抽屉的关闭按钮

  • 能控制抽屉的打开方向

  • 关闭抽屉时是否销毁里面的子元素(这个问题是工作中频繁遇到的问题)

  • 指定 Drawer 挂载的 HTML 节点, 可以将抽屉挂载在任何元素上

  • 点击蒙层可以控制是否允许关闭抽屉

  • 能控制遮罩层的展示

  • 能自定义抽屉弹出层样式

  • 可以设置抽屉弹出层宽度

  • 能控制弹出层层级

  • 能控制抽屉弹出方向(上下左右)

  • 点击关闭按钮时能提供回调供开发者进行相关操作

需求收集好之后,作为一个有追求的程序员, 会得出如下线框图:

如何使用React Portals实现一个功能强大的抽屉组件

对于react选手来说,如果没用typescript,建议大家都用PropTypes, 它是react内置的类型检测工具,我们可以直接在项目中导入.  vue有自带的属性检测方式,这里就不一一介绍了.

通过以上需求分析, 是不是觉得一个抽屉组件要实现这么多功能很复杂呢?  确实有点复杂,但是不要怕,有了上面精确的需求分析,我们只需要一步步按照功能点实现就好了.对于我们常用的table组件,  modal组件等其实也需要考虑到很多使用场景和功能点, 比如antd的table组件暴露了几十个属性,如果不好好理清具体的需求,  实现这样的组件是非常麻烦的.接下来我们就来看看具体实现.

2. 基于react实现一个Drawer组件

2.1. Drawer组件框架设计

首先我们先根据需求将组件框架写好,这样后面写业务逻辑会更清晰:

import PropTypes from 'prop-types' import styles from './index.less'  /**  * Drawer 抽屉组件  * @param {visible} bool 抽屉是否可见  * @param {closable} bool 是否显示右上角的关闭按钮  * @param {destroyOnClose} bool 关闭时销毁里面的子元素  * @param {getContainer} HTMLElement 指定 Drawer 挂载的 HTML 节点, false 为挂载在当前 dom  * @param {maskClosable} bool 点击蒙层是否允许关闭抽屉  * @param {mask} bool 是否展示遮罩  * @param {drawerStyle} object 用来设置抽屉弹出层样式  * @param {width} number|string 弹出层宽度  * @param {zIndex} number 弹出层层级  * @param {placement} string 抽屉方向  * @param {onClose} string 点击关闭时的回调  */ function Drawer(props) {   const {      closable = true,      destroyOnClose,      getContainer = document.body,      maskClosable = true,      mask = true,      drawerStyle,      width = '300px',     zIndex = 10,     placement = 'right',      onClose,     children   } = props    const childDom = (     <div className={styles.xDrawerWrap}>       <div className={styles.xDrawerMask} ></div>       <div          className={styles.xDrawerContent}          {           children         }         {           !!closable && <span className={styles.xCloseBtn}>X</span>         }       </div>     </div>   )   return childDom }  export default Drawer

有了这个框架,我们来一步步往里面实现内容吧.

2.2 实现visible, closable, onClose, mask, maskClosable, width, zIndex,  drawerStyle

之所以要先实现这几个功能,是因为他们实现都比较简单,不会牵扯到其他复杂逻辑.只需要对外暴露属性并使用属性即可. 具体实现如下:

function Drawer(props) {   const {      closable = true,      destroyOnClose,      getContainer = document.body,      maskClosable = true,      mask = true,      drawerStyle,      width = '300px',     zIndex = 10,     placement = 'right',      onClose,     children   } = props    let [visible, setVisible] = useState(props.visible)    const handleClose = () => {     setVisible(false)     onClose && onClose()   }    useEffect(() => {     setVisible(props.visible)   }, [props.visible])    const childDom = (     <div        className={styles.xDrawerWrap}        style={{         width: visible ? '100%' : '0',         zIndex       }}     >       { !!mask && <div className={styles.xDrawerMask} onClick={maskClosable ? handleClose : null}></div> }       <div          className={styles.xDrawerContent}          style={{           width,           ...drawerStyle         }}>         { children }         {           !!closable && <span className={styles.xCloseBtn} onClick={handleClose}>X</span>         }       </div>     </div>   )   return childDom }

上述实现过程值得注意的就是我们组件设计采用了react hooks技术, 在这里用到了useState, useEffect,  如果大家不懂的可以去官网学习, 非常简单,如果有不懂的可以和笔者交流或者在评论区提问.  抽屉动画我们通过控制抽屉内容的宽度来实现,配合overflow:hidden, 后面我会单独附上css代码供大家参考.

2.3 实现destroyOnClose

destroyOnClose主要是用来清除组件缓存,比较常用的场景就是输入文本,比如当我是的抽屉的内容是一个表单创建页面时,我们关闭抽屉希望表单中用户输入的内容清空,保证下次进入时用户能重新创建,  但是实际情况是如果我们不销毁抽屉里的子组件, 子组件内容不会清空,用户下次打开时开始之前的输入,这明显不合理. 如下图所示:

 如何使用React Portals实现一个功能强大的抽屉组件

要想清除缓存,首先就要要内部组件重新渲染,所以我们可以通过一个state来控制,如果用户明确指定了关闭时要销毁组件,那么我们就更新这个state,从而这个子元素也就不会有缓存了.具体实现如下:

function Drawer(props) {   // ...   let [isDesChild, setIsDesChild] = useState(false)    const handleClose = () => {     // ...     if(destroyOnClose) {       setIsDesChild(true)     }   }    useEffect(() => {     // ...     setIsDesChild(false)   }, [props.visible])    const childDom = (     <div className={styles.xDrawerWrap}>       <div className={styles.xDrawerContent}          {           isDesChild ? null : children         }       </div>     </div>   )   return childDom }

上述代码中我们省略了部分不相关代码, 主要来关注isDesChild和setIsDesChild,  这个属性用来根据用户传入的destroyOnClose属性俩判断是否该更新这个state,  如果destroyOnClose为true,说明要更新,那么此时当用户点击关闭按钮的时候, 组件将重新渲染, 在用户再次点开抽屉时,  我们根据props.visible的变化,来重新让子组件渲染出来,这样就实现了组件卸载的完整流程.

2.4 实现getContainer

getContainer主要用来控制抽屉组件的渲染位置,默认会渲染到body下,  为了提供更灵活的配置,我们需要让抽屉可以渲染到任何元素下,这样又怎么实现呢? 这块实现我们可以采用React Portals来实现,具体api介绍如下:

  • Portal 提供了一种将子节点渲染到存在于父组件以外的 DOM 节点的优秀的方案。第一个参数(child)是任何可渲染的 React  子元素,例如一个元素,字符串或 fragment。第二个参数(container)是一个 DOM 元素。

具体使用如下:

render() {   // `domNode` 是一个可以在任何位置的有效 DOM 节点。   return ReactDOM.createPortal(     this.props.children,     domNode   ); }

所以基于这个api我们就能把抽屉渲染到任何元素下了, 具体实现如下:

const childDom = (     <div        className={styles.xDrawerWrap}        style={{         position: getContainer === false ? 'absolute' : 'fixed',         width: visible ? '100%' : '0',         zIndex       }}     >       { !!mask && <div className={styles.xDrawerMask} onClick={maskClosable ? handleClose : null}></div> }       <div          className={styles.xDrawerContent}          style={{           width,           [placement]: visible ? 0 : '-100%',           ...drawerStyle         }}>         {           isDesChild ? null : children         }         {           !!closable && <span className={styles.xCloseBtn} onClick={handleClose}>X</span>         }       </div>     </div>   )    return getContainer === false ? childDom              : ReactDOM.createPortal(childDom, getContainer)

因为这里getContainer要支持3种情况,一种是用户不配置属性,那么默认就挂载到body下,还有就是用户传的值为false, 那么就为最近的父元素,  他如果传一个dom元素,那么将挂载到该元素下,所以以上代码我们会分情况考虑,还有一点要注意,当抽屉打开时,我们要让父元素溢出隐藏,不让其滚动,所以我们在这里要设置一下:

useEffect(() => {     setVisible(() => {       if(getContainer !== false && props.visible) {         getContainer.style.overflow = 'hidden'       }       return props.visible     })     setIsDesChild(false)   }, [props.visible, getContainer])

当关闭时恢复逻辑父级的overflow, 避免影响外部样式:

const handleClose = () => {     onClose && onClose()     setVisible((prev) => {       if(getContainer !== false && prev) {         getContainer.style.overflow = 'auto'       }       return false     })     if(destroyOnClose) {       setIsDesChild(true)     }   }

2.5 实现placement

placement主要用来控制抽屉的弹出方向, 可以从左弹出,也可以从右弹出,  实现过程也比较简单,我们主要要更具属性动态修改定位属性即可,这里我们会用到es新版的新特性,对象的变量属性. 核心代码如下:

<div    className={styles.xDrawerContent}    style={{     width,     [placement]: visible ? 0 : '-100%',     ...drawerStyle     }}>  </div>

这样,无论是上下左右,都可以完美实现了.

2.6 健壮性支持, 我们采用react提供的propTypes工具:

import PropTypes from 'prop-types' // ... Drawer.propTypes = {   visible: PropTypes.bool,   closable: PropTypes.bool,    destroyOnClose: PropTypes.bool,    getContainer: PropTypes.element,    maskClosable: PropTypes.bool,    mask: PropTypes.bool,    drawerStyle: PropTypes.object,    width: PropTypes.oneOfType([     PropTypes.string,     PropTypes.number   ]),   zIndex: PropTypes.number,   placement: PropTypes.string,    onClose: PropTypes.func }

关于prop-types的使用官网上有很详细的案例,这里说一点就是oneOfType的用法, 它用来支持一个组件可能是多种类型中的一个.  组件相关css代码如下:

.xDrawerWrap {   top: 0;   height: 100vh;   overflow: hidden;   .xDrawerMask {     position: absolute;     left: 0;     right: 0;     top: 0;     bottom: 0;     background-color: rgba(0, 0, 0, .5);   }   .xDrawerContent {     position: absolute;     top: 0;     padding: 16px;     height: 100%;     transition: all .3s;     background-color: #fff;     box-shadow: 0 0 20px rgba(0,0,0, .2);     .xCloseBtn {       position: absolute;       top: 10px;       right: 10px;       color: #ccc;       cursor: pointer;     }   } }

“如何使用React Portals实现一个功能强大的抽屉组件”的内容就介绍到这里了,感谢大家的阅读。如果想了解更多行业相关的知识可以关注亿速云网站,小编将为大家输出更多高质量的实用文章!

向AI问一下细节

免责声明:本站发布的内容(图片、视频和文字)以原创、转载和分享为主,文章观点不代表本网站立场,如果涉及侵权请联系站长邮箱:is@yisu.com进行举报,并提供相关证据,一经查实,将立刻删除涉嫌侵权内容。

AI