当前位置: 首页 > 科技观察

配合ReactPortals实现一个强大的抽屉组件

时间:2023-03-19 18:17:14 科技观察

Text在开始组件设计之前,希望大家有一定的css3和js基础,了解基本的react/vue语法。先来看看实现的组件效果:1.组件设计思路根据笔者之前总结的组件设计原则,我们第一步是确认需求。一个抽屉(Drawer)组件会有如下需求:可以控制抽屉是否可见可以手动配置抽屉的关闭按钮控制抽屉的打开方向抽屉关闭时是否销毁里面的子元素(这个问题是工作中经常遇到的)指定Drawer挂载的HTML节点,可以将drawer挂载到任意元素上点击mask控制是否允许关闭drawer时可以控制mask层的显示。您可以自定义抽屉弹出层的样式。您可以设置抽屉弹出层的宽度。您可以控制弹出层级别。可以控制抽屉的弹出方向(上、下、左、右)。当点击关闭按钮时,可以提供回调给开发者进行相关操作。收藏之后,作为一个有抱负的程序员,你会得到如下线框图:对于react玩家,如果你不会使用typescript,建议大家使用PropTypes,它是react中内置的类型检测工具,我们可以直接在项目中导入。Vue有自己的属性检测方式,这里就不一一介绍了。通过上面的需求分析,你是不是觉得一个抽屉组件实现这么多功能是不是很复杂?确实有点复杂,但是不用怕,有了上面的精准需求分析,我们只需要根据功能点一步步实现即可。对于我们常用的表格组件、模态组件等,我们也需要考虑很多的使用场景和功能点。比如antd的table组件,暴露了几十个,如果不明确具体需求,实现这样的组件是很麻烦的。接下来,我们来看一下具体的实现。2、基于react2.1实现一个Drawer组件。抽屉组件框架设计首先,我们先根据需求写好组件框架,这样后面的业务逻辑会更清晰:importPropTypesfrom'prop-types'importstylesfrom'./index.less'/***Drawer抽屉组件*@param{visible}bool抽屉是否可见*@param{closable}bool右上角是否显示关闭按钮*@param{destroyOnClose}bool关闭时销毁里面子元素*@param{getContainer}HTMLElement指定Drawer挂载的HTML节点,false表示挂载在当前dom上*@param{maskClosable}bool是否允许点击mask时关闭drawer*@param{mask}bool是否显示遮罩*@param{drawerStyle}对象用于设置抽屉弹出层样式*@param{width}number|string弹出层宽度*@param{zIndex}numberpop-uplayerlevel*@param{placement}stringdrawerdirection*@param{onClose}string被点击关闭时的回调*/functionDrawer(props){const{closable=true,destroyOnClose,getContainer=document.body,maskClosable=true,mask=true,drawerStyle,width='300px',zIndex=10,placement='right',onClose,children}=propsconstchildDom=(/div>X}

)returnchildDom}exportdefaultDrawer有了这个框架,让我们实现一步一步地讲内容。2.2实现visible、closable、onClose、mask、maskClosable、width、zIndex、drawerStyle之所以需要先实现这些功能,是因为实现起来比较简单,不会涉及到其他复杂的逻辑。您只需要公开和使用属性。具体实现如下:functionDrawer(props){const{closable=true,destroyOnClose,getContainer=document.body,maskClosable=true,mask=true,drawerStyle,width='300px',zIndex=10,placement='right',onClose,children}=propslet[可见,设置可见]=useState(props.visible)constandleClose=()=>{setVisible(false)onClose&&onClose()}useEffect(()=>{setVisible(props.visible)},[props.visible])constchildDom=({!!mask&&
}{children}{!!closable&&X}
)returnchildDom}上面的实现过程值得注意的是我们的组件设计使用了reacthooks技术,这里使用了useState,useEffect,如果不明白可以去官网学习,很简单,不懂的可以和作者交流或者提问s在评论区。我们通过控制抽屉内容的宽度来实现抽屉动画。对于overflow:hidden,我会单独附上css代码,供大家参考。2.3实现destroyOnClosedstroyOnClose主要用于清除组件缓存。比较常见的场景是输入文本。例如,当我的抽屉内容是一个表单创建页面时,我们关闭抽屉,希望用户在表单中输入的内容被清空,以保证下次用户输入时可以重新创建,但是实际情况是,如果我们不销毁抽屉里的子组件,子组件的内容不会被清除,用户下次启动前的输入显然是不合理的。如下图所示:清除缓存,首先需要重新渲染内部组件,所以我们可以通过一个state来控制。如果用户明确指定组件在关闭时应该被销毁,那么我们将更新状态,这样这个子元素就不会有缓存。具体实现如下:(()=>{//...setIsDesChild(false)},[props.visible])constchildDom=()returnchildDom}在上面的代码中,我们省略了一些不相关的代码,主要关注isDesChild和setIsDesChild。该属性用于根据用户传入的destroyOnClose属性判断是否更新状态。如果destroyOnClose为真,则需要更新描述。然后当用户点击关闭按钮时,组件将被重新渲染。当用户再次点击抽屉时,我们会根据props.visible的变化重新渲染子组件,从而实现组件卸载的完整过程。2.4getContainer的实现getContainer主要用来控制drawer组件的渲染位置,默认渲染在body下面。为了提供更灵活的配置,我们需要允许抽屉在任意元素下渲染。如何实现?实现我们可以使用ReactPortals来实现,具体api介绍如下:Portal提供了一个很好的解决方案,可以将子节点渲染到存在于父组件之外的DOM节点上。第一个参数(child)是任何可渲染的React子元素,例如元素、字符串或片段。第二个参数(容器)是一个DOM元素。具体用法如下:render(){//`domNode`是一个有效的DOM节点,可以在任意位置。returnReactDOM.createPortal(this.props.children,domNode);}所以基于这个api,我们可以渲染任意元素下的抽屉。具体实现如下:constchildDom=({!!mask&&}{isDesChild?null:children}{!!closable&&X}}div>)returngetContainer===false?childDom:ReactDOM.createPortal(childDom,getContainer)因为这里getContainer要支持三种情况,一种是用户没有配置属性,那么挂载在body下面默认情况下,用户传递的值为false,那么就是最近的父元素,如果传递的是dom元素,那么就会挂载到该元素下,所以上面的代码我们根据情况考虑,以及还有一点要注意的是,当抽屉打开后,我们想让父元素溢出并隐藏,所以这里需要设置:useEffect(()=>{setVisible(()=>{if(getContainer!==false&&props.visible){getContainer.style.overflow='隐藏den'}returnprops.visible})setIsDesChild(false)},[props.visible,getContainer])关闭时,恢复逻辑父级的溢出,避免影响外部样式:constandleClose=()=>{onClose&&onClose()setVisible((prev)=>{if(getContainer!==false&&prev){getContainer.style.overflow='auto'}returnfalse})if(destroyOnClose){setIsDesChild(true)}}2.5placement的实现placement主要用来控制抽屉的弹出方向,可以从左边弹出,也可以从右边弹出。实现过程比较简单。我们主要需要动态修改定位属性。这里我们就用到新版es的新特性,对象的可变属性。核心代码如下:这样,无论是上、下、左、右,都能完美实现2.6Robustness支持,我们使用react提供的propTypes工具:importPropTypesfrom'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.nu??mber]),zIndex:PropTypes.number,placement:PropTypes。字符串,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:0020pxrgba(0,0,0,.2);.xCloseBtn{position:absolute;top:10px;right:10px;color:#ccc;cursor:pointer;}}}通过以上步骤,一个强大的抽屉组件完成,关于css模块和classnames的使用可以去官网自行学习。这很简单。有不懂的可以在评论区提问。Prompt)、badge(标志)、table(表格)、tooltip(工具提示)、Skeleton(骨架屏幕)、Message(全局提示)、form(表单表单)、switch(开关)、date/calendar、二维码识别器组件等组件,来回顾一下作者多年的组件化历程