likes
comments
collection
share

antd Modal组件的封装与使用技巧

作者站长头像
站长
· 阅读数 60

弹框(Modal)作为一个基础的能力,我们在项目开发中,非常频繁的使用到这个组件。而无论antd还是其他的组件库,基本都会提供这个组件,因为使用频率真的是很高。但是这么高频的组件,其实依然有二次封装的必要,也有一些使用技巧需要注意的。

全局化配置

由于Modal不隶属于我们页面的根节点,所以即使我们一般会在项目的根节点进行全局的语言、主题等的配置,例如:

<ConfigProvider locale={ zhCN }>
  <div className={ Style.root }>
    <Router>
      <RouterGuard>
        <ElementRoute/>
      </RouterGuard>
    </Router>
  </div>
</ConfigProvider>

但是这样的设置,对于Modal中的内容是不会生效的。为了避免每个弹框都去进行类似的设置,我们可以封装一个公共的弹框组件,统一进行配置:

interface IProps extends ModalProps {
  setOpen: Function
}
export default function ModalWrapper({children, setOpen, ...rest}: IProps) {
  const wrapper = <ConfigProvider locale={ zhCN }>
    <MixcConfigProvider
      theme={{
        token: {
          controlHeight: 32 // 按钮控件高度
        }
      }}
    >{children}</MixcConfigProvider>
  </ConfigProvider>
  
  return <Modal
    keyboard={false}
    maskClosable={false}
    centered={true}
    {...rest}
    >{wrapper}</Modal>
}

例如上面这样,可以统一进行弹框类型的全局配置,并且可以进行一些公共属性的配置(例如:居中设置)。

退出页面关闭

由于我们的项目,现在一般都是当页应用,所以我们切换路由的时候,只是更新了路由组件节点下的内容,而Modal是挂载在另外一个节点下的,所以会出现回退的情况下,弹框依然在的问题。而这往往不是我们想要的,所以我们可以在封装的公共弹框组件中,添加上公共的监听事件,当页面路径发生变化时,关闭弹框

const curPathnameRef = useRef(location.pathname);
  const fake = useCallback(function() {
    if (location.pathname !== curPathnameRef.current) {
      setOpen(false)
    }
  }, [])
  useEffect(() => {
    window.addEventListener('popstate', fake);
    return () => {
      window.removeEventListener('popstate', fake);
    }
  }, [])

动态挂载

对于Modal,我们往往是这样使用的,例如官方例子:

import React, { useState } from 'react';
import { Button, Modal } from '@mixc/components';

function App() {
  const [isModalOpen, setIsModalOpen] = useState(false);

  const showModal = () => {
    setIsModalOpen(true);
  };

  const handleOk = () => {
    setIsModalOpen(false);
  };

  const handleCancel = () => {
    setIsModalOpen(false);
  };

  return (
    <>
      <Button type="primary" onClick={showModal}>
        Open Modal
      </Button>
      <Modal title="标题名称" open={isModalOpen} onOk={handleOk} onCancel={handleCancel}>
        <p>Some contents...</p>
        <p>Some contents...</p>
        <p>Some contents...</p>
      </Modal>
    </>
  );
}

export default App;

而这样开发,衍生的副作用其实比较多,例如:页面一加载,该页面的所有弹框也同步加载,如果弹框里面还有接口请求,也会同步发起。而我们理想的情况下,应该是点击了入口按钮才执行对应的弹框逻辑并展示。而且在进行权限控制的时候,如果是上面这种使用方法,即使按钮由于没有权限点击不了,但是弹框里面的初始化逻辑却依然执行了。

动态挂载可以比较好的解决这个问题,只有点击了按钮,才会挂载弹框组件,然后执行弹框初始化逻辑。

关于如何实现动态挂载,其实官方已经提供了一些具备动态挂载的方法,例如Modal.info。而我们实现的基本原理其实也和Modal.info大同小异。

查看源码,我们首先看到:

Modal.info = function infoFn(props: ModalFuncProps) {
  return confirm(withInfo(props));
};

很精简的代码,接下来我们需要看看confirm的实现:

antd Modal组件的封装与使用技巧

这份代码基本就包含了我们需要的最重要的逻辑了,弹框的基础逻辑无非就是:render挂载渲染,destory销毁。我们先来看看render。

render

antd Modal组件的封装与使用技巧

render中,我们需要了解的是reactRender的逻辑,至于ConfirmDialog其实就是一个二次封装的dialog。而dialog的基本原理,其实就是类似利用createPortal创建组件然后渲染到document.body中,这里就不再详述。

reactRender是基于另外一个底层库rc-util,核心代码在第6行(root.render(node)):

function modernRender(node: React.ReactElement, container: ContainerType) {
  toggleWarning(true);
  const root = container[MARK] || createRoot(container);
  toggleWarning(false);

  root.render(node);

  container[MARK] = root;
}

到此,render的逻辑就清晰了,通过createRoot创建根节点,然后往创建的根节点中挂载dialog,而dialog是基于createPortal实现的。

destroy

function destroy(...args: any[]) {
    const triggerCancel = args.some(param => param && param.triggerCancel);
    if (config.onCancel && triggerCancel) {
      config.onCancel(() => {}, ...args.slice(1));
    }
    for (let i = 0; i < destroyFns.length; i++) {
      const fn = destroyFns[i];
      // eslint-disable-next-line @typescript-eslint/no-use-before-define
      if (fn === close) {
        destroyFns.splice(i, 1);
        break;
      }
    }

    reactUnmount(container);
  }

destroy对我们来说,最重要的是第15行,reactUnmount同样是rc-util中的方法,其核心代码并不复杂:

async function modernUnmount(container: ContainerType) {
  // Delay to unmount to avoid React 18 sync warning
  return Promise.resolve().then(() => {
    container[MARK]?.unmount();

    delete container[MARK];
  });
}

container[MARK]记录的是刚刚render中通过createRoot创建的根节点,然后destroy则是调用根节点的unmount进行销毁。

动态挂载弹框方法封装

基于上面的原理,我们就可以封装自己的方法了,毕竟Modal.info之类的官方方法并不能满足我们的诉求。主要的代码如下:

import React, { useEffect } from 'react';
import { Modal } from 'antd';
import { createPortal } from 'react-dom';
import { createRoot } from 'react-dom/client';

function ModalWrapper({ handleClose }: any) {
  // code

  useEffect(() => {
    // 接口请求之类的逻辑
  }, []);

  return (
    <Modal
      onCancel={handleClose}
      onOk={() => {
        // code
        handleClose();
      }}
    ></Modal>
  );
}

export default function showModal(props: any) {
  const root = createRoot(document.createElement('div'));
  root.render(
    createPortal(
      <ModalWrapper
        {...props}
        handleClose={() => {
          root.unmount();
        }}
      ></ModalWrapper>,
      document.body
    )
  );
}

这样,我们只需要在点击某按钮要显示该弹框的时候,调用showModal()即可,与之相关的逻辑只有在调用后才会执行。

转载自:https://juejin.cn/post/7372135071979290639
评论
请登录