antd Modal组件的封装与使用技巧
弹框(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的实现:
这份代码基本就包含了我们需要的最重要的逻辑了,弹框的基础逻辑无非就是:render挂载渲染,destory销毁。我们先来看看render。
render
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