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

《精通react-vue组件设计》配合ReactPortals实现强大的抽屉组件

时间:2023-03-13 15:15:09 科技观察

前言本文是作者撰写的关于组件设计的第六篇文章。许多复杂组件的必要方法之一。通过组件设计的过程,你将接触到一个完整的、健壮的组件设计思想和方法,在实现组件的过程中逐渐深入了解react/vue的高级知识和技能并掌握,并做到在企业实际工作中游刃有余。之所以写组件设计相关的文章,是因为作为一名优秀的前端工程师,面对各种繁琐重复的工作,我们不应该“一步步“辛苦”劳动”,而是要总结出一套基于现有前端开发经验的高效开发方式,作为数据驱动的领导者,react/vue等MVVM框架的出现,帮助我们在工作中减少了很多冗余代码,万物皆组件的理念很流行,为了让工程师有更多时间考虑业务和产品迭代,我们必须掌握优质组件设计的思路和方法。所以笔者会花时间总结各种业务场景的设计思路和方法以下组件,并使用原生框架的语法实现各种常用组件的开发,希望能让前端新手或者有一定工作经验的朋友有所收获。如果你是不熟悉react/vue组件设计原理的可以,可以参考我之前的组件设计系列文章:《精通react/vue组件设计》5分钟实现一个Tag(标签)组件和Empty(空状态)组件《精通react/vue组件设计》使用纯css制作类似materialUI的按钮点击动画并封装成为react组件《精通react/vue组件设计》快速实现可自定义的进度条组件《精通react/vue组件设计》基于jsoneditor二次封装,可实时预览的json编辑器组件(react版)。在开始组件设计之前,希望大家对css3和js有一定的基础,了解基本的react/vue语法。我们先来看看实现。最终的组件效果:1.组件设计思路根据笔者之前总结的组件设计原则,我们第一步是确认需求。一个抽屉(Drawer)组件会有如下需求:可以控制抽屉是否可见,可以手动配置抽屉关闭按钮可以控制抽屉打开的方向,抽屉关闭时是否销毁里面的子元素(这个问题在工作中经常遇到)。指定抽屉挂载的HTML节点,抽屉可以挂载在任意元素上。点击遮罩层可以控制抽屉是否允许关闭可以控制遮罩层的显示可以自定义抽屉弹出层的样式可以设置抽屉弹出层的宽度可以控制弹出的层级-up层可以控制抽屉的弹出方向(上、下、左、右)当关闭按钮被点击时,可以提供回调给开发者执行相关操作需求收集后,作为一个有抱负的人程序员,你会得到如下线框图:对于react玩家,如果你不会使用typescript,建议大家使用PropTypes,它是react中内置的类型检测工具。我们可以直接将其导入到项目中。Vue有自己的属性检测方法,这里就不一一介绍了。通过上面的需求分析,你是不是觉得一个抽屉组件实现这么多功能是不是很复杂?确实有点复杂,不过不用怕,有了上面精准的需求分析,我们只需要一步步实现功能点即可。对于我们常用的表格组件,模态组件等,我们还需要考虑很多的使用场景和功能点,比如antd的表格组件暴露的属性就有几十个,如果不明确具体的需求,它实现这样的组件很麻烦。接下来我们来看一下具体的实现。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*@param{mask}bool是否显示遮罩Cover*@param{drawerStyle}对象用于设置抽屉弹出层样式*@param{width}number|string弹出层宽度*@param{zIndex}number弹出层层级*@param{placement}string抽屉方向*@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[可见,setVisible]=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实现destroyOnClosedestroyOnClose主要用于清除组件缓存。比较常见的场景是输入文本。例如,当我的抽屉内容是一个表单创建页面时,我们关闭抽屉,希望用户在表单中输入的内容被清空,以保证下次用户输入时可以重新创建,但是实际情况是,如果我们不销毁抽屉里的子组件,子组件的内容不会被清除,用户下次启动前的输入显然是不合理的。如下图所示:清除缓存,首先需要重新渲染内部组件,所以我们可以通过一个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='hidden'}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.6Robust支持,我们使用react提供的propTypes工具:importPropTypesfrom'prop-types'//。..Drawer.propTypes={visible:PropTypes.bool,closable:PropTypes.bool,destroyOnClose:PropTypes.bool,getContainer:PropTypes.element,maskClosable:PropTypepes.bool,掩码:PropTypes.bool,drawerStyle:PropTypes.object,宽度: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-颜色: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;}}}通过以上步骤,一个强大的drawer组件就完成了,关于代码中css模块和classnames的使用就可以了自己去官网学习,很简单。有不懂的可以在评论区提问,笔者看到后会第一时间解答。扩展目前作者已经在npm上发布了完整的组件库,大家可以通过npm安装包使用:npmi@alex_xu/xui//使用import{Button,Alert}from'@alex_xu/xui'在线文档地址:xui—基于react的轻量级UI组件库npm包地址:@alex_xu/xui最后,后续作者实现了modal(模态窗口)、alert(警告提示)、badge(标志)、table(表单)、tooltip(工具提示)、Skeleton(骨架屏)、Message(全局提示)、form(表单表单)、switch(开关)、date/calendar、二维码识别器组件等组件,来福作者多年的组件化之旅。