文章详情

短信预约-IT技能 免费直播动态提醒

请输入下面的图形验证码

提交验证

短信预约提醒成功

《精通react/vue组件设计》之配合React Portals实现一个功能强大的抽屉组件

2024-12-03 08:10

关注

作为数据驱动的领导者react/vue等MVVM框架的出现,帮我们减少了工作中大量的冗余代码, 一切皆组件的思想深得人心. 为了让工程师们有更多的时间去考虑业务和产品迭代,我们不得不掌握高质量组件设计的思路和方法.所以笔者将花时间去总结各种业务场景下的组件的设计思路和方法,并用原生框架的语法去实现各种常用组件的开发,希望能让前端新手或者有一定工作经验的朋友能有所收获.

如果对于react/vue组件设计原理不熟悉的,可以参考我的之前写的组件设计系列文章:

正文

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


1. 组件设计思路

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

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


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

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

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

2.1. Drawer组件框架设计

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

  1. import PropTypes from 'prop-types' 
  2. import styles from './index.less' 
  3.  
  4.  
  5. function Drawer(props) { 
  6.   const {  
  7.     closable = true,  
  8.     destroyOnClose,  
  9.     getContainer = document.body,  
  10.     maskClosable = true,  
  11.     mask = true,  
  12.     drawerStyle,  
  13.     width = '300px'
  14.     zIndex = 10, 
  15.     placement = 'right',  
  16.     onClose, 
  17.     children 
  18.   } = props 
  19.  
  20.   const childDom = ( 
  21.      
  22.       
 
  •       
  •         className={styles.xDrawerContent}  
  •         { 
  •           children 
  •         } 
  •         { 
  •           !!closable && X 
  •         } 
  •       
  •  
  •      
  •   ) 
  •   return childDom 
  •  
  • export default Drawer 
  •  有了这个框架,我们来一步步往里面实现内容吧.

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

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

    1. function Drawer(props) { 
    2.   const {  
    3.     closable = true,  
    4.     destroyOnClose,  
    5.     getContainer = document.body,  
    6.     maskClosable = true,  
    7.     mask = true,  
    8.     drawerStyle,  
    9.     width = '300px'
    10.     zIndex = 10, 
    11.     placement = 'right',  
    12.     onClose, 
    13.     children 
    14.   } = props 
    15.  
    16.   let [visible, setVisible] = useState(props.visible) 
    17.  
    18.   const handleClose = () => { 
    19.     setVisible(false
    20.     onClose && onClose() 
    21.   } 
    22.  
    23.   useEffect(() => { 
    24.     setVisible(props.visible) 
    25.   }, [props.visible]) 
    26.  
    27.   const childDom = ( 
    28.     
    29.       className={styles.xDrawerWrap}  
    30.       style={{ 
    31.         width: visible ? '100%' : '0'
    32.         zIndex 
    33.       }} 
    34.     > 
    35.       { !!mask && null}> } 
    36.       
    37.         className={styles.xDrawerContent}  
    38.         style={{ 
    39.           width, 
    40.           ...drawerStyle 
    41.         }}> 
    42.         { children } 
    43.         { 
    44.           !!closable && X 
    45.         } 
    46.        
    47.      
    48.   ) 
    49.   return childDom 

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

    2.3 实现destroyOnClose

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

     

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

    1. function Drawer(props) { 
    2.   // ... 
    3.   let [isDesChild, setIsDesChild] = useState(false
    4.  
    5.   const handleClose = () => { 
    6.     // ... 
    7.     if(destroyOnClose) { 
    8.       setIsDesChild(true
    9.     } 
    10.   } 
    11.  
    12.   useEffect(() => { 
    13.     // ... 
    14.     setIsDesChild(false
    15.   }, [props.visible]) 
    16.  
    17.   const childDom = ( 
    18.      
    19.       
    20.         { 
    21.           isDesChild ? null : children 
    22.         } 
    23.        
    24.      
    25.   ) 
    26.   return childDom 

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

    2.4 实现getContainer

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

    具体使用如下:

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

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

    1. const childDom = ( 
    2.     
    3.       className={styles.xDrawerWrap}  
    4.       style={{ 
    5.         position: getContainer === false ? 'absolute' : 'fixed'
    6.         width: visible ? '100%' : '0'
    7.         zIndex 
    8.       }} 
    9.     > 
    10.       { !!mask && null}>
     } 
  •       
  •         className={styles.xDrawerContent}  
  •         style={{ 
  •           width, 
  •           [placement]: visible ? 0 : '-100%'
  •           ...drawerStyle 
  •         }}> 
  •         { 
  •           isDesChild ? null : children 
  •         } 
  •         { 
  •           !!closable && X 
  •         } 
  •        
  •      
  •   ) 
  •  
  •   return getContainer === false ? childDom  
  •             : ReactDOM.createPortal(childDom, getContainer) 
  •  因为这里getContainer要支持3种情况,一种是用户不配置属性,那么默认就挂载到body下,还有就是用户传的值为false, 那么就为最近的父元素, 他如果传一个dom元素,那么将挂载到该元素下,所以以上代码我们会分情况考虑,还有一点要注意,当抽屉打开时,我们要让父元素溢出隐藏,不让其滚动,所以我们在这里要设置一下:

    1. useEffect(() => { 
    2.     setVisible(() => { 
    3.       if(getContainer !== false && props.visible) { 
    4.         getContainer.style.overflow = 'hidden' 
    5.       } 
    6.       return props.visible 
    7.     }) 
    8.     setIsDesChild(false
    9.   }, [props.visible, getContainer]) 

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

    1. const handleClose = () => { 
    2.     onClose && onClose() 
    3.     setVisible((prev) => { 
    4.       if(getContainer !== false && prev) { 
    5.         getContainer.style.overflow = 'auto' 
    6.       } 
    7.       return false 
    8.     }) 
    9.     if(destroyOnClose) { 
    10.       setIsDesChild(true
    11.     } 
    12.   } 

    2.5 实现placement

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

    1.   className={styles.xDrawerContent}  
    2.   style={{ 
    3.     width, 
    4.     [placement]: visible ? 0 : '-100%'
    5.     ...drawerStyle 
    6.     }}> 
    7.   

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

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

    1. import PropTypes from 'prop-types' 
    2. // ... 
    3. Drawer.propTypes = { 
    4.   visible: PropTypes.bool, 
    5.   closable: PropTypes.bool,  
    6.   destroyOnClose: PropTypes.bool,  
    7.   getContainer: PropTypes.element,  
    8.   maskClosable: PropTypes.bool,  
    9.   mask: PropTypes.bool,  
    10.   drawerStyle: PropTypes.object,  
    11.   width: PropTypes.oneOfType([ 
    12.     PropTypes.string, 
    13.     PropTypes.number 
    14.   ]), 
    15.   zIndex: PropTypes.number, 
    16.   placement: PropTypes.string,  
    17.   onClose: PropTypes.func 

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

    1. .xDrawerWrap { 
    2.   top: 0; 
    3.   height: 100vh; 
    4.   overflow: hidden; 
    5.   .xDrawerMask { 
    6.     position: absolute
    7.     left: 0; 
    8.     right: 0; 
    9.     top: 0; 
    10.     bottom: 0; 
    11.     background-color: rgba(0, 0, 0, .5); 
    12.   } 
    13.   .xDrawerContent { 
    14.     position: absolute
    15.     top: 0; 
    16.     padding: 16px; 
    17.     height: 100%; 
    18.     transition: all .3s; 
    19.     background-color: #fff; 
    20.     box-shadow: 0 0 20px rgba(0,0,0, .2); 
    21.     .xCloseBtn { 
    22.       position: absolute
    23.       top: 10px; 
    24.       right: 10px; 
    25.       color: #ccc; 
    26.       cursor: pointer; 
    27.     } 
    28.   } 

    通过以上步骤, 一个功能强大的的drawer组件就完成了,关于代码中的css module和classnames的使用大家可以自己去官网学习,非常简单.如果不懂的可以在评论区提问,笔者看到后会第一时间解答.

    扩展

    目前笔者已经将完成的组件库发布到npm上了,大家可以通过npm安装包的方式使用:

    1. npm i @alex_xu/xui 
    2.  
    3. // 使用 
    4. import { Button, Alert } from '@alex_xu/xui' 

    在线文档地址: xui——基于react的轻量级UI组件库

    npm包地址: @alex_xu/xui

    最后

    后续笔者已经实现

    等组件, 来复盘笔者多年的组件化之旅.

     

    来源:趣谈前端内容投诉

    免责声明:

    ① 本站未注明“稿件来源”的信息均来自网络整理。其文字、图片和音视频稿件的所属权归原作者所有。本站收集整理出于非商业性的教育和科研之目的,并不意味着本站赞同其观点或证实其内容的真实性。仅作为临时的测试数据,供内部测试之用。本站并未授权任何人以任何方式主动获取本站任何信息。

    ② 本站未注明“稿件来源”的临时测试数据将在测试完成后最终做删除处理。有问题或投稿请发送至: 邮箱/279061341@qq.com QQ/279061341

    软考中级精品资料免费领

    • 2024年上半年信息系统项目管理师第二批次真题及答案解析(完整版)

      难度     813人已做
      查看
    • 【考后总结】2024年5月26日信息系统项目管理师第2批次考情分析

      难度     354人已做
      查看
    • 【考后总结】2024年5月25日信息系统项目管理师第1批次考情分析

      难度     318人已做
      查看
    • 2024年上半年软考高项第一、二批次真题考点汇总(完整版)

      难度     435人已做
      查看
    • 2024年上半年系统架构设计师考试综合知识真题

      难度     224人已做
      查看

    相关文章

    发现更多好内容

    猜你喜欢

    AI推送时光机
    位置:首页-资讯-后端开发
    咦!没有更多了?去看看其它编程学习网 内容吧
    首页课程
    资料下载
    问答资讯