在开始组件设计之前希望大家对css3和js有一定的配合基础,并了解基本的react/vue语法.我们先看看实现后的组件效果:
按照之前笔者总结的组件设计原则,我们第一步是要确认需求. 一个抽屉(Drawer)组件会有如下需求点:
能控制抽屉是否可见 能手动配置抽屉的关闭按钮 能控制抽屉的打开方向 关闭抽屉时是否销毁里面的子元素(这个问题是工作中频繁遇到的问题) 指定 Drawer 挂载的 HTML 节点, 可以将抽屉挂载在任何元素上 点击蒙层可以控制是否允许关闭抽屉 能控制遮罩层的展示 能自定义抽屉弹出层样式 可以设置抽屉弹出层宽度 能控制弹出层层级 能控制抽屉弹出方向(上下左右) 点击关闭按钮时能提供回调供开发者进行相关操作需求收集好之后,作为一个有追求的程序员, 会得出如下线框图:
对于react选手来说,如果没用typescript,建议大家都用PropTypes, 它是react内置的类型检测工具,我们可以直接在项目中导入. vue有自带的属性检测方式,这里就不一一介绍了.
通过以上需求分析, 是不是觉得一个抽屉组件要实现这么多功能很复杂呢? 确实有点复杂,但是不要怕,有了上面精确的需求分析,我们只需要一步步按照功能点实现就好了.对于我们常用的table组件, modal组件等其实也需要考虑到很多使用场景和功能点, 比如antd的table组件暴露了几十个属性,如果不好好理清具体的需求, 实现这样的组件是非常麻烦的.接下来我们就来看看具体实现.
首先我们先根据需求将组件框架写好,这样后面写业务逻辑会更清晰:
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有了这个框架,我们来一步步往里面实现内容吧.
之所以要先实现这几个功能,是因为他们实现都比较简单,不会牵扯到其他复杂逻辑.只需要对外暴露属性并使用属性即可. 具体实现如下:
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代码供大家参考.
destroyOnClose主要是用来清除组件缓存,比较常用的场景就是输入文本,比如当我是的抽屉的内容是一个表单创建页面时,我们关闭抽屉希望表单中用户输入的内容清空,保证下次进入时用户能重新创建, 但是实际情况是如果我们不销毁抽屉里的子组件, 子组件内容不会清空,用户下次打开时开始之前的输入,这明显不合理. 如下图所示:
要想清除缓存,首先就要要内部组件重新渲染,所以我们可以通过一个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的变化,来重新让子组件渲染出来,这样就实现了组件卸载的完整流程.
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) } }placement主要用来控制抽屉的弹出方向, 可以从左弹出,也可以从右弹出, 实现过程也比较简单,我们主要要更具属性动态修改定位属性即可,这里我们会用到es新版的新特性,对象的变量属性. 核心代码如下:
<div className={ styles.xDrawerContent} style={ { width, [placement]: visible ? 0 : -100%, ...drawerStyle }}> </div>这样,无论是上下左右,都可以完美实现了.
关于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; } } }通过以上步骤, 一个功能强大的的drawer组件就完成了,关于代码中的css module和classnames的使用大家可以自己去官网学习,非常简单.如果不懂的可以在评论区提问,笔者看到后会第一时间解答.
后续笔者将会继续实现
modal(模态窗), alert(警告提示), badge(徽标), table(表格), tooltip(工具提示条), Skeleton(骨架屏), Message(全局提示), form(form表单), switch(开关), 日期/日历, 二维码识别器组件等组件, 来复盘笔者多年的组件化之旅。
个功云南idc服务商