几百行代码,优雅的管理弹窗

几百行代码,优雅的管理弹窗

技术博客 admin 128 浏览

前言

一个大屏项目,项目特点是有很多弹窗,并且各个弹窗的通用性很高,会在项目个各模块子模块相互调用,甚至弹窗也会相互调用

简单说明就是:

模块a ----> 弹窗b ---> 弹窗c ---->弹窗b

模块a ----> 弹窗c ---> 弹窗b

模块c ----> 弹窗b ---> 弹窗a

而且打开下一个弹窗的时候需要关闭下层弹窗,避免开的弹窗太多,观感不好

反正就是这样神奇的调用链

一开始是由各个不同的小伙伴负责不同的模块,于是问题逐渐出现

  • 弹窗被重复开发,不知道其他模块有这个弹窗导致的页面重复开发
  • 弹窗导入混乱
  • 沟通上的矛盾,每个弹窗的维护者不知道其他弹窗需要哪些参数,或者这个弹窗开发者提供的弹窗能满足自己的需求吗
  • 性能问题,弹窗被关闭打开其他的弹窗,在弹窗内容复杂的情况下出现卡顿

压死骆驼最后的稻草,是产品的需求

要求在弹窗左上角添加面包屑导航,能回到上个弹窗

于是找到了我解决以上问题

开始

以上需求,很明显需要用一个弹窗将所有弹窗管理起来,然后小伙伴们可以统一看到注册的弹窗,然后在全局任意的地方都可以调用弹窗,而不用去import 弹窗

重要的是将弹窗的逻辑和实际的业务解耦

开发全局弹窗

GlobalModalServices.tsx

tsx
代码解读
复制代码
import HncyModal from '@/components/HncyModal'; import { useBoolean } from 'ahooks'; import { Flex } from 'antd'; import React, { forwardRef, useEffect, useImperativeHandle, useMemo, useState, } from 'react'; import styles from './index.less'; import Registry, { ModalKey } from './registry'; import type { GlobalModalConfig, GlobalModalPrideAction, ModalStackType, } from './type'; let modalIndex = 0; /** 获取弹窗内容 */ const getModalElement = (modalKey: ModalKey) => { if (modalKey in Registry) { return Registry[modalKey]; } else { throw new Error(`Modal ${modalKey} not found`); } }; /** @module 全居弹窗服务 */ const GlobalModalServices: React.ForwardRefRenderFunction< GlobalModalPrideAction, any > = (_, ref) => { const [modalStack, setModalStack] = useState<ModalStackType[]>([]); const [open, { setFalse, setTrue }] = useBoolean(false); useEffect(() => { if (modalStack.length === 0) { setFalse(); } else { setTrue(); } }, [modalStack]); /** @see GlobalModalServicesModalMethods.push */ const push = (modalKey: ModalKey, config?: GlobalModalConfig) => { /** 生成弹窗id */ const modalId = `modal_${modalIndex++}`; const { defaultConfig, defaultProps, render } = getModalElement(modalKey); const newConfig = { ...defaultConfig, ...config }; const newProps = { ...defaultProps, ...(config?.props ?? {}) }; let modalElement: JSX.Element | undefined; if (newConfig.keepalive) { /** * 保存在栈中,不重复渲染。此实现无法避免组件的卸载与状态保持,仅节约重复渲染时间 * 暂存 @see https://github.com/CJY0208/react-activation * 等待有时间实现keepalive功能 * */ modalElement = render(newProps); } const newModalStack = { ...newConfig, modalId, props: newProps, modalKey, modalElement, render, }; setModalStack((pre) => [...pre, newModalStack]); return modalId; }; /** @see GlobalModalServicesModalMethods.go */ const go = (modalId: string) => { const index = modalStack.findIndex((item) => item.modalId === modalId); if (index === -1) { throw new Error('modal not found'); } else { setModalStack((pre) => pre.filter((_, i) => i <= index)); } }; /** @see GlobalModalServicesModalMethods.remove */ const remove = (id: string) => { setModalStack((prev) => { return prev.filter((item) => item.modalId !== id); }); }; /** @see GlobalModalServicesModalMethods.setOptions */ const setOptionsById = (id: string, config: any) => { setModalStack((prev) => { return prev.map((item) => { if (item.modalId === id) { return { ...item, ...config, }; } return item; }); }); }; /** 设置options */ const setOptions = (options: Partial<GlobalModalConfig>) => { setModalStack((prev) => { return prev.map((item, index) => { if (index === prev.length - 1) { return { ...item, ...options, }; } return item; }); }); }; useImperativeHandle(ref, () => ({ close, setOptions, setOptionsById, push, goBack, remove, go, clear, })); /** @see GlobalModalServicesModalMethods.clear */ const clear = () => { setModalStack([]); }; /** * @description modal返回 * */ const goBack = () => { setModalStack((prev) => { return prev.slice(0, prev.length - 1); }); }; /** 获取当前弹窗的内容 */ const renderModal = () => { const curModal = modalStack.at(-1); /** 避免重复渲染 */ if (curModal?.modalElement) { return curModal.modalElement; } return curModal?.render(curModal.props); }; const modalOptions = useMemo(() => { return modalStack.at(-1) ?? ({} as ModalStackType); }, [modalStack]); /** 标题渲染 */ const titleRender = () => { const { headerLeft } = modalOptions; return ( <Flex align="center"> <div className={styles.modalTitle}> {modalStack?.length && ( <> {modalStack.map((item, index: number) => ( <span key={item.modalId} style={{ cursor: 'pointer' }} onClick={() => go(item.modalId)} > {item.title} {modalStack.length - 1 !== index && ( <span style={{ margin: '0 5px' }}>/</span> )} </span> ))} </> )} </div> {headerLeft ? typeof headerLeft === 'function' ? headerLeft() : headerLeft : undefined} </Flex> ); }; const close = () => { setFalse(); setTimeout(() => { clear(); }, 300); }; return ( <HncyModal onCancel={close} width={modalOptions.w ?? 200} height={modalOptions.h ?? 200} open={open} titleRender={titleRender} > {open && renderModal()} </HncyModal> ); }; export default forwardRef(GlobalModalServices);

主要工作是提供对弹窗堆栈的管理方法,以及添加面包屑导航

方法主要下面这些

ts
代码解读
复制代码
import { ModalKey } from './registry'; export interface GlobalModalConfig<T = any> { title?: string; w?: number; h?: number; /** 显示标题栏 */ showHeader?: boolean; /** 自定义标题栏左侧内容 */ headerLeft?: React.ReactNode | (() => React.ReactNode); /** 自定义标题栏右侧内容 */ headerRight?: React.ReactNode; // 是否保持弹窗状态,默认为false,弹窗关闭后会自动销毁 keepalive?: boolean; props?: T; } export type ModalMapping = { render: (props: GlobalModalConfig['props']) => JSX.Element; defaultProps?: any; defaultConfig?: Partial<GlobalModalConfig>; }; /** 全局弹窗方法 */ export type GlobalModalServicesModalMethods = { /** * 推送一个弹窗 * @param modalKey 弹窗类型 * @param config 弹窗参数 * @param keepalive 是否保持弹窗状态,默认为false,弹窗关闭后会自动销毁 * @returns modalId 弹窗id,使用modalId进行后续操作 * */ push: (modalKey: ModalKey, config?: GlobalModalConfig) => string; /** * 关闭一个弹窗 * @params modalId 弹窗id * @description 弹窗关闭后,会自动销毁 * */ remove: (modalId: string) => void; /** * 跳转一个弹窗 * @params modalId 弹窗id * @description 跳转到指定的弹窗,如果弹窗不存在,则会自动创建 * */ go: (modalId: string) => void; /** * 清空所有弹窗 * */ clear: () => void; /** 设置弹窗基础信息 */ setOptionsById: (modalId: string, config: GlobalModalConfig) => void; }; export interface GlobalModalPrideAction extends GlobalModalServicesModalMethods { /** 返回上一页 */ goBack(): void; /** 关闭弹窗 */ close(): void; /** 设置弹窗参数 */ setOptions(options: Partial<GlobalModalConfig>): void; } export interface ModalStackType extends GlobalModalConfig, ModalMapping { modalKey: string; modalElement?: JSX.Element; modalId: string; }

modal 本身基于antd modal封装

tsx
代码解读
复制代码
import type { ModalProps } from 'antd'; import { Modal } from 'antd'; import styles from './index.less'; interface HncyModalProps extends ModalProps { height?: number; minHeight?: number; padding?: string | number; bgColor?: React.CSSProperties; bodyStyle?: React.CSSProperties; titleRender?: () => JSX.Element; } const HncyModal = (props: HncyModalProps) => { const { children, title, onCancel, height, minHeight, style, padding, titleRender, bodyStyle, ...reset } = props; return ( <Modal onCancel={onCancel} footer={null} destroyOnClose maskClosable classNames={{ mask: styles.mask, }} {...reset} style={{ pointerEvents: 'auto', ...style }} wrapClassName="hncyModal" > <div className={styles.modalWrap} style={{ height, minHeight, padding, ...bodyStyle }} > <div className={styles.head}> {titleRender ? titleRender() : title && <p>{title}</p>} </div> <div className={styles.content} style={{ height: title || titleRender ? 'calc(100% - 80px)' : '100%', }} > {children} </div> </div> </Modal> ); }; export default HncyModal;

管理注册表

新增的弹窗往注册表一塞就完事儿

tsx
代码解读
复制代码
//@ts-nocheck import React from 'react'; import DistrictIndex from './Modals/DistrictIndex'; import KeyArea from './Modals/KeyArea'; import KeyAreaDetail from './Modals/KeyAreaDetail'; import { ModalMapping } from './type'; export type ModalKey = keyof typeof Registry; /** @see ModalMapping 弹窗注册在这里 */ const Registry = { districtIndex: { defaultConfig: { w: 2756, h: 846, title: '行业指数', }, defaultProps: {}, render: (props: any) => React.cloneElement(<DistrictIndex />, props), }, keyArea: { defaultConfig: { w: 2771, h: 846, title: '重点区域', keepalive: true, }, defaultProps: {}, render: (props: any) => React.cloneElement(<KeyArea />, props), }, KeyAreaDetail: { defaultConfig: { w: 2771, h: 846, title: '重点区域详情', }, defaultProps: {}, render: (props: any) => React.cloneElement(<KeyAreaDetail />, props), }, }; export default Registry;

添加提供者

tsx
代码解读
复制代码
import React, { PropsWithChildren, useEffect, useRef, useState } from 'react'; import GlobalModalServices from '.'; import type { GlobalModalPrideAction } from './type'; const GlobalModalContext = React.createContext( {} as { dispatch: GlobalModalPrideAction; }, ); export const useGlobalModalServices = () => React.useContext(GlobalModalContext); /** 全局弹窗服务提供者 */ const GlobalModalServicesProvider: React.FC<PropsWithChildren> = (props) => { const [dispatch, setDispatch] = useState<GlobalModalPrideAction>(); const ref = useRef<GlobalModalPrideAction>(null); useEffect(() => { if (ref.current && !dispatch) { setDispatch(ref.current); } }, [ref.current]); return ( <GlobalModalContext.Provider value={{ dispatch: dispatch!, }} > <GlobalModalServices ref={ref} /> {props.children} </GlobalModalContext.Provider> ); }; export default GlobalModalServicesProvider;

在项目最外层包裹住提供者

tsx
代码解读
复制代码
//app.tsx xxx <GlobalModalServicesProvider> <App message={{ maxCount: 1 }} style={{ width: '100%', height: '100%' }} ></App> </GlobalModalServicesProvider> xxx

在项目中使用

tsx
代码解读
复制代码
const { dispatch } = useGlobalModalServices(); //xxxx <div className={styles.moreBtn} onClick={() => { dispatch.push('districtIndex', { title: selectDistrict.objectName + '运行情况', props: { data: selectDistrict, }, }); }} > 查看更多 </div> //xxx

问题解决了,现在小伙伴们能更加专注业务了

源文:几百行代码,优雅的管理弹窗

如有侵权请联系站点删除!

Technical cooperation service hotline, welcome to inquire!