Skip to content
On this page

优化篇-公共弹窗拆解优化,让职能更加单一


本章节主要是对公共弹窗的拆解优化,如果你对本章节内容兴趣不大,可以快速阅读或跳过。

公共弹窗拆解—流程梳理及职责划分

想必小伙伴们还记得我们在点击导出PDF按钮时,会出现一个确认弹窗。如下图所示

image.png

这是一个常见的功能,我们在第八章添加了 <MyModal.Comfirm /> 组件实现了上图的弹窗效果。但这仅仅只是一个最简单的场景,如果面对更为复杂多样的弹窗效果,就不适用了。让我们来预测一下,之后我们可能会出现什么弹窗场景呢?

  • 场景一

当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,存在蒙层,点击弹窗区域之外(即蒙层),弹窗关闭

  • 场景二

当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,存在蒙层

  • 场景三

当点击 Button 按钮,弹窗打开,弹窗的位置是水平垂直居中,没有蒙层

  • 场景四

当点击 Button 按钮,弹窗打开,弹窗的位置是垂直居中,位于顶部,没有蒙层,点击弹窗之外区域,弹窗关闭

当然还有其他场景,以目前我们封装的 renderer/common/components/MyModal/MyConfirm 组件仅能满足场景二,其他场景均不满足。所以我们需要进行优化处理,我们必须捋一捋我们的流程。

流程梳理

通过下面流程图,我们来捋一捋这个流程

image.png

下面是一段伪代码:

ts
import React, { useState, useEffect } from 'react';

function DownloadPdf() {
  // 控制弹窗显隐
  const [showModal, setShowModal] = useState(false);

  // 监听 click 事件,在组件卸载时移除
  useEffect(() => {
    window.addEventListener('click', handleClickBody, false);
    return () => {
      window.removeEventListener('click', handleClickBody, false);
    };
  }, []);

  // 在点击弹窗之外区域,关闭弹窗
  const handleClickBody = () => setShowModal(false);

  // 点击按钮,显示弹窗
  const handleClickButton = () => setShowModal(true);

  return (
    <div>
      <button onClick={handleClickButton}>导出简历</button>

      {showModal && (
        <div
          styleName="mask"
          onClick={(e) => {
            // 需要阻止冒泡,不然点击弹窗区域会导致弹窗关闭
            e.stopPropagation();
            e.nativeEvent.stopImmediatePropagation();
          }}
        >
          <DownLoadModal onCancel={() => setShowModal(false)} />
        </div>
      )}
    </div>
  );
}

整体来讲,我们弹窗的主要流程大致如上所示。

抽丝剥茧

动动我们的小奶袋瓜,结合四种场景,想一下点击弹窗区域之外,关闭弹窗这个交互效果,是否能做成通用呢?换个说法,我们能否能抽象成:给你一个 elementRef,点击 elementRef 区域之外,触发回调事件。

举几个例子:

  • 点击弹窗区域之外,关闭弹窗(这里的 elementRef 元素就是弹窗组件,回调事件就是关闭弹窗)
  • 点击侧边栏区域之外,给个提示框(这里的 elementRef 元素就是侧边栏组件,回调事件就是显示提示框)
  • 点击简历头像,给个夸奖(这里的 elementRef 元素就是简历头像,回调事件就是一句夸奖提示)

我们肯定不想在每个实现此交互效果的组件中,内部实现一套 监听事件,所以我们能否将其抽离出来呢?思考思考如何实现?

还有一个问题,我们想一想,这个位置是否可选?比如我期望该显示的弹窗水平垂直居中,或者头部居中、底部居中,再或者我期望该蒙层的背景色能进行改变?我们肯定不期望每次都自己 Ctrl+C 拷贝一份蒙层相关的代码吧?所以我们能否将其抽离出来呢?思考思考如何实现?

useClickAwayHook

react-useuseClickAway 影响,自己动手实现了一个 useClickAwayHook,即 triggers a callback when user clicks outside the target element.

具体实现是怎样的呢?我们先前往 renderer/common 目录下,新增一个名为 hook 的文件夹,顾名思义,不再解释,我们新增一个名为 useClickAway.ts 文件,让我们来写一下实现的代码

ts
// renderer/common/hook/useClickAway.ts
import { useEffect, useRef, useState } from 'react';

/**
 * @description 点击元素之外区域关闭
 */
function useClickAway(initIsVisible: boolean) {
  const ref = useRef() as React.MutableRefObject<HTMLDivElement>;
  const [componentVisible, setComponentVisible] = useState(initIsVisible);

  const onClickOutSide = (event: any) => {
    if (ref.current && !ref.current.contains(event.target)) {
      setComponentVisible(false);
    }
  };

  useEffect(() => {
    document.addEventListener('click', onClickOutSide, true);
    return () => {
      document.removeEventListener('click', onClickOutSide, true);
    };
  });

  return { ref, componentVisible, setComponentVisible };
}

export default useClickAway;

使用上非常简单,下面以导出PDF弹窗确定为例,看看如何使用

ts
// renderer/container/resume/ResumeAction/index.tsx
import useClickAway from '@common/hook/useClickAway';

function ResumeAction() {
  const { ref, componentVisible, setComponentVisible } = useClickAway(false);

  return (
    <div styleName="actions">
      <div styleName="back" onClick={onBack}>
        返回
      </div>
      <MyButton
        size="middle"
        className="export-btn"
        onClick={() => setComponentVisible(true)}
      >
        导出PDF
      </MyButton>
      {componentVisible && (
        <MyModal.Confirm
          eleRef={ref}
          title="确定要打印简历吗?"
          description="请确保信息的正确,目前仅支持单页打印哦~"
          config={{
            cancelBtn: {
              isShow: true,
              callback: () => setComponentVisible(false),
            },
            submitBtn: {
              isShow: true,
              callback: exportPdf,
            },
          }}
        />
      )}
    </div>
  );
}

👉 具体相关代码可看此 commit

MyMaskHoc

前往 renderer/common 目录下,新增一个名为 hoc 的文件夹,顾名思义,不再解释,我们新增一个名为 MyMaskHoc 文件夹,让我们来写一下实现的代码

ts
// renderer/common/hoc/MyMaskHoc/index.tsx
/**
 * @description 为目标组件添加一层蒙层
 */
import React from 'react';
import './index.less';
import classnames from 'classnames';
export type Position = 'top' | 'bottom' | 'center';

const MyMaskHoc =
  (WrappedComponent: React.ComponentType) =>
  (hocProps: { position?: Position; backgroundColor?: string }) => {
    return class extends React.Component {
      getProps = () => ({
        ...this.props,
      });
      render() {
        const position = hocProps ? hocProps?.position : 'center';
        const backgroundColor = hocProps
          ? hocProps?.backgroundColor
          : 'rgba(0, 0, 0, 0.78)';

        return (
          <div
            styleName="vis-mask"
            style={{ backgroundColor: backgroundColor }}
          >
            <div
              styleName={classnames({
                top: position === 'top',
                center: position === 'center',
                bottom: position === 'bottom',
              })}
            >
              <WrappedComponent {...this.getProps()} />
            </div>
          </div>
        );
      }
    };
  };

export default MyMaskHoc;

👉 具体相关代码可看此 commit

使用就比较简单了,我们只给需要添加蒙层的组件,包裹一下 MyMaskHoc 即可。这边就不一一改造了,感兴趣的小伙伴可以私下对项目中的弹窗蒙层进行改造一波,下面给出一个简单的使用例子

ts
// 测试例子
import React from 'react';
import MyMaskHoc from '@src/common/hoc/MyMaskHoc';

function TestModal() {
  return <div>我是测试带有蒙层的代码</div>;
}

export default MyMaskHoc(TestModal)({
  position: 'top',
  backgroundColor: 'red',
});

小测试:上面的 TestModal 实现了蒙层效果,那么我想将 useClickAway 添加进来,实现交互级的效果,这代码该如何写呢?

总结

上面我们对公共弹窗的交互进行了分析,从而实现了 useClickAwayHookMyMaskHoc,至于两者如何使用,我在这里当成是一个小测试。

本章节的重点在于从一个常见的场景出发,去分析一整套流程下来,哪部分可以进行抽离分割,从而达到高内聚、低耦合的结果。关于此章节,如果有疑问,可以在评论区指出。