手把手教你使用 React 封装一个 Modal 组件

602 阅读3分钟

前言

在我的个人博客前台的项目中,自己封装了一些组件。为了能让自己更加熟悉开发组件的流程和思想,决定打算记录下来,同时也可以分享给大家。

分析组件的结构

一个 Modal 组件大概分为三个部分,分别是 header、body 和 footer。其中 header 和footer 是固定在上面和下面的。body 中的部分是 children。结构图如下:

设计组件的 Props

从上图来看 Modal 组件大概有 title、visible、children、onOk、onCancel 等属性。title 可以控制组件的标题,visble 控制组件的显示与隐藏,children 放入组件的 body 部分,onOk 是处理确认按钮的回调函数,onCancel 是隐藏组件的回调函数。代码如下:

interface IModalProps { 
    visible: boolean; 
    title: string; 
    okText: string; 
    cancelText: string;
    onCancel: () => void;
    onOk: () => void; 
    footer: boolean; 
}

okText 、cancelText是可以控制确认和取消按钮的文案。footer 可以控制组件是否显示底部的部分。

使用 createPortal

将组件渲染到其他结点中 - createPortal 是 ReactDOM 的一个方法 - createPortal 可以将子节点渲染到 DOM 节点中的方式,该节点存在于 DOM 组件的层次结构之外。 - 因此 createPortal 适合脱离文档流的组件,特别是 position: absolute 与 position: fixed的组件。比如模态框,通知,警告等。 我们做的 Modal 组件正是需要这样的特性,再也不用担心组件会被其他组件的样式影响啦~

interface IProps { ... }
const Modal: FC<IProps> = ({ visible, children, title, okText, cancelText, onCancel, onOk, footer = true, }) => {
  const modalEleRef = useRef<HTMLElement | null>(null);
  useEffect(() => {
    const node = document.getElementById("W-Modal");
     if (node) {
      modalEleRef.current = node;
    } else {
      modalEleRef.current = document.createElement("div");
      modalEleRef.current.setAttribute("id", "W-Modal");
      document.body.appendChild(modalEleRef.current);
    }
  }, []);

  if (!modalEleRef.current) return null;

  return createPortal(
  visible &&
    <div className="fixed w-full h-full top-0 backdrop-blur-sm z-10 bg-black/30" onClick={(e) => { onCancel?.(); }} >
      <div onClick={(e) => e.stopPropagation()} className={`z-50 m-auto h-full md:w-600 md:h-auto md:mt-32 bg-box-dark md:rounded-xl md:border md:border-gray-400/40 p-5 md:hover:border-blue-600 md:hover:shadow-xl md:hover:shadow-blue-600/10 transition-all duration-300`} >
      {/* header */}
      <div className="flex justify-between mb-3">
        <h3 className=" text-xl font-bold text-blue-600"> {title || "标题"} </h3>
        <CloseOutlined onClick={onCancel} className="text-lg font-extrabold hover:text-blue-600 transition-all duration-300 cursor-pointer" />
      </div>
      {/* body */}
      <div className="mt-2 mb-3">
        {children}
      </div>
      {/* footer */}
      {footer &&
        (<div className="mt-2 flex justify-end">
          <div className="mr-4 text-base cursor-pointer px-3 py-1 rounded-md hover:bg-gray-800/30 hover:text-red-400 transition-all duration-300" onClick={onCancel} >
             {cancelText || "取消"}
          </div>
          <div className="text-base cursor-pointer px-3 py-1 rounded-md hover:bg-gray-800/30 hover:text-blue-600 transition-all duration-300" onClick={onOk} >
            {okText || "确定"}
          </div>
        </div>
        )}
      </div>
    </div>,
    modalEleRef.current );
};

组件就这样完成啦,非常简单!样式使用的是 Tailwind CSS。一开始使用这个样式库我觉得有点鸡肋,后面用着用着还挺香的😋需要注意的是在组件挂载完时,需要去判断是否有存放 Modal 组件的结点,没有的话则去创建一个结点,如果有则去获取该结点。

添加动画

虽然组件的功能已经完成,但是组件缺少动画效果,在交互上会显得很生硬。所以接下来我们给组件添加动画效果。将采用 react-transition-group 这个动画库去实现。 下面是 Fade 动画组件代码:

interface IProps {
  visible?: boolean;
  duration?: number;
  style?: CSSProperties;
  onEntered?: () => void;
  onExited?: () => void;
  children: React.ReactElement;
}

const Fade: FC<IProps> = ({
  visible,
  duration,
  children,
  onEntered,
  onExited,
  style
}) => (
  <div className="component-transition-fade">
    <CSSTransition
      in={visible}
      timeout={duration ?? 300}
      classNames="fade"
      mountOnEnter
      unmountOnExit
      onEntered={onEntered}
      onExited={onExited}
    >
      {React.cloneElement(children, { style: { ...style } })}{' '}
    </CSSTransition>{' '}
  </div>
);

样式代码:

:root { 
    --duration-base: 0.3s; 
    --timing-function-enter: ease-out;
    --timing-function-leave: ease-in; 
} 

@keyframes fade-in { 
    from { 
        opacity: 0;
    }
    to { 
        opacity: 1;
    }
} 

@keyframes fade-out { 
    from { 
        opacity: 1;
    } 
    to { 
        opacity: 0;
    }
} 

.component-transition-fade { 
    width: 100%; 
    height: 100%;
} 

.component-transition-fade .fade-enter-active{ 
    animation: var(--duration-base) fade-in both var(--timing-function-enter);
} 

.component-transition-fade .fade-exit-active{
    animation: var(--duration-base) fade-out both var(--timing-function-leave);
} 

现在只需要给组件嵌套在该 Fade 动画组件里就好了:

const Modal: FC<IProps> = ({...}) => { 
    ... 
    return createPortal(
        <Fade visible={visible}> 
            ...原来组件的代码 
        </Fade>, 
        modalEleRef.current
    ) 
}

最终效果如下图:

引用

动画库官方文档地址:reactcommunity.org/react-trans…

动画组件:(使用react-transition-group制作常用的动画过渡组件 - 掘金)