Drawer组件封装示例
常用的三方组件库中称之为抽屉,平时开发中使用Modal组件应该比较居多,使用这个drawer组建的应用场景应该比较少,近期我也是封装自己的一套组件库,由于没有设计在风格上面就参考的ant组件库,但是功能都是自己从逻辑代码慢慢敲出来的。
这次我就带来一个封装Drawer组件的全过程。
封装组件第一步
封装一个组件我们不能一上来就开始写代码,我们应该先去思考这个组件需要一些什么功能。以及为使用者提供一些什么样的方法属性等等,都需要我们在封装组件的时候去考虑。只有在封装组件之前去尽可能考虑清除这些问题在封装的过程中才能思路明确, 常常说思考几个小时真正写代码的时候其实也就一会。
Drawer组件应该考虑的事情:
1:组件应该挂载到什么位置,是应该挂载到使用当前页面的某个节点吗?
2:组件应该有哪些属性,哪些功能,以及方法
3:组件应该提供哪些自定义修改,用户在使用过程中肯定是要去修改我们组件的。
4:组件动画如何实现?
5:组件在创建完成之后如何销毁?
…
细节还有很多很多这里就不一一列举了,我们就带着这几个问题去看如何从零封装Drawer组件
创建组件
创建我们原始组件文件
const Drawer: React.FC = forwardRef((props, ref) => {
return <></>
}
上面我们考虑的第一个问题这个组件应该挂载到何处?首先我们想一下如果我们挂载到了使用页面下面,那么我们这个组件就是使用页面下的一个子节点,那么我们这个组建的样式必然会被使用页面的样式影响到。所以这里我们这里就不能挂载到使用页面。那我们能挂载到哪里呢?这个时候我们通常的做法都是挂载到body下面既能保证样式不受影响,整体的dom结构还是在body下面。
这边我是用的是react,react有一个方法可以把dom发送到body称之为传送门 createPortal使用这个就可以把我们的dom发送到body下面了,这个很多组件都用得到所以这里我封装成了工具组件。
Partoal工具组件封装
import React, { forwardRef, useEffect, useState } from 'react';
import ReactDOM from 'react-dom';
export type ContainerType = Element | DocumentFragment;
type Iprops = {
visible: boolean;
children: any;
destroyOnClose?: boolean;
};
const Partoal: React.FC<Partial<Iprops>> = forwardRef((props, ref) => {
const { visible, children, destroyOnClose } = props;
const createContainer = () => {
const container = document.createElement('div');
container.style.position = 'absolute';
container.style.top = '0';
container.style.left = '0';
container.style.width = '100%';
return container;
};
const [container, setContainer] = useState<ContainerType>(() =>
createContainer(),
);
let timer: any = null;
useEffect(() => {
const customizeContainer = createContainer();
setContainer(customizeContainer ?? null);
return () => {
clearTimeout(timer);
};
}, []);
let containerBox = container ?? createContainer();
console.log(container);
useEffect(() => {
if (visible) {
if (containerBox) {
document.body.appendChild(containerBox);
}
} else if (!visible && destroyOnClose) {
//延迟注销组件 完成组件动画
timer = setTimeout(() => {
if (containerBox.parentElement) {
containerBox?.parentElement.removeChild(containerBox);
}
}, 300);
}
}, [visible]);
let reffedChildren = children;
if (ref) {
reffedChildren = React.cloneElement(children as any, {});
}
return <>{ReactDOM.createPortal(<>{reffedChildren}</>, containerBox)}</>;
});
export default Partoal;
第二步:工具函数准备好了之后我们就可以写Drawer组件里面的功能了
这个是我参考了ant的一些部分功能我认为在使用中比较常用的功能
type Iprops = {
visible: boolean;
zIndex?: number;
placement?: string;//组件出现方向 这个可以定义enum更好
handleClose?: (flag: boolean) => void;
width?: string;
height?: string;
children: any;
maskClosable?: boolean;//蒙层是否关闭
extra?: React.ReactNode;//右上角的按钮
title?: string;
destroyOnClose?: boolean;//关闭是否销毁
footer?: React.ReactNode;
closable?: boolean;
};
第三步:组件的哪些属性可以更改
这个可以再属性中加一些classname,style等等属性这里就不列举了
第四步:动画如何实现
这里我是用了CSSTransition组件库用起来还是很简单。这个是我的蒙层动画附上css。
<CSSTransition
timeout={300}
classNames={'fadee'}
in={visible}
onEnter={() => {
setShow(true);
}}
onExited={() => {
setShow(false);
}}
>
<Overly
maskClosable={maskClosable}
modalDisplay={show}
onChange={(flag) => {
handleClose?.(flag);
}}
/>
</CSSTransition>
/* enter是入场前的刹那(点击按钮),appear指页面第一次加载前的一刹那(自动) */
.fadee-enter,
.fadee-appear {
opacity: 0;
}
/* //enter-active指入场后到入场结束的过程,appear-active则是页面第一次加载自动执行 */
.fadee-enter-active,
.fadee-appear-active {
opacity: 1;
transition: opacity 0.3s ease-in;
}
/* //入场动画执行完毕后,保持状态 */
.fadee-enter-done {
opacity: 1;
}
/* //同理,出场前的一刹那,以下就不详细解释了,一样的道理 */
.fadee-exit {
opacity: 1;
}
.fadee-exit-active {
opacity: 0;
transition: opacity 0.3s ease-out;
}
.fadee-exit-done {
opacity: 0;
}
第五步:组件创建之后如何销毁?
组件我们是在工具函数中创建的所以销毁肯定也只在工具函数中。这就是销毁函数的核心代码逻辑,在销毁之前一定要先保证小隐藏组件动画执行完成之后在销毁组件,不然组件动画不会被正常显示。
useEffect(() => {
if (visible) {
if (containerBox) {
document.body.appendChild(containerBox);
}
} else if (!visible && destroyOnClose) {
//延迟注销组件 完成组件动画
timer = setTimeout(() => {
if (containerBox.parentElement) {
containerBox?.parentElement.removeChild(containerBox);
}
}, 300);
}
}, [visible]);
附上组件完整代码,这个组件要封装还有很多小细节可以去做。
/*
* @Date: 2023-08-08 11:13:39
* @Auth: 463997479@qq.com
* @LastEditors: 463997479@qq.com
* @LastEditTime: 2023-08-09 11:45:48
* @FilePath: \reactui\src\Drawer\index.tsx
*/
import { CloseOutlined } from '@ant-design/icons';
import classNames from 'classnames';
import React, {
forwardRef,
useImperativeHandle,
useMemo,
useState,
} from 'react';
import { CSSTransition } from 'react-transition-group';
import Portal from '../Dialog/Partoal';
import Overly from '../Overly';
type Iprops = {
visible: boolean;
zIndex?: number;
placement?: string;
handleClose?: (flag: boolean) => void;
width?: string;
height?: string;
children: any;
maskClosable?: boolean;
extra?: React.ReactNode;
title?: string;
destroyOnClose?: boolean;
footer?: React.ReactNode;
closable?: boolean;
};
const Drawer: React.FC<Iprops> = forwardRef((props, ref) => {
const {
visible,
zIndex,
handleClose,
placement = 'left',
width,
height,
maskClosable,
extra,
title,
destroyOnClose,
footer,
closable,
} = props;
//控制动画显示 在动画进入之前设置true在动画结束之后设置false控制节点display
//不然display一下就变成了none节点一下消失动画就不会执行
const [show, setShow] = useState(false);
const getAnimationName = (arg: string) => {
return {
animationNameIn: `drawerAnimation${arg}In`,
animationNameOut: `drawerAnimation${arg}Out`,
};
};
const getContentSize = useMemo(() => {
return placement === 'left' || placement === 'right'
? { width: width }
: { height: height };
}, [placement]);
const { animationNameIn, animationNameOut } = getAnimationName(placement);
const drawerClassName = classNames('dcqc-drawer-wapper', {
[`dcqc-drawer-wapper-${placement}`]: placement,
});
useImperativeHandle(ref, () => {
return {
getContentSize,
};
});
return (
<Portal destroyOnClose={destroyOnClose} visible={visible}>
<>
<div
style={
{
'--animationNameIn': animationNameIn,
'--animationNameOut': animationNameOut,
'--zIndex': zIndex,
} as React.CSSProperties
}
className="dcqc-drawer"
>
<CSSTransition
timeout={300}
classNames={'fadee'}
in={visible}
onEnter={() => {
setShow(true);
}}
onExited={() => {
setShow(false);
}}
>
<Overly
maskClosable={maskClosable}
modalDisplay={show}
onChange={(flag) => {
handleClose?.(flag);
}}
/>
</CSSTransition>
<CSSTransition
timeout={300}
classNames={'drawer-animation'}
in={visible}
onEnter={() => {
setShow(true);
}}
onExited={() => {
setShow(false);
}}
>
<div
style={
{
display: show ? 'block' : 'none',
...getContentSize,
} as any
}
className={drawerClassName}
>
<div className="dcqc-drawer-content">
<div className="dcqc-drawer-header">
<div className="dcqc-drawer-left">
{closable ? (
<span
onClick={() => {
handleClose?.(false);
}}
className="dcqc-drawer-close"
>
<CloseOutlined
style={{ color: '#00000073', marginRight: '20.0px' }}
className="dialog-icon"
/>
</span>
) : null}
<span>{title}</span>
</div>
<div className="dcqc-drawer-extra">{extra}</div>
</div>
<div className="dcqc-drawer-body">{props.children}</div>
{footer ? (
<div className="dcqc-drawer-footer">{footer}</div>
) : null}
</div>
</div>
</CSSTransition>
</div>
</>
</Portal>
);
});
export default Drawer;
Drawer.defaultProps = {
zIndex: 999,
placement: 'left',
width: '350.0px',
height: '350.0px',
maskClosable: false,
destroyOnClose: false,
closable: true,
};
组件完成例子
到此Drawer组件封装完成了,后续我也会带来更多有趣的组件封装。