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

前言

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

简单说明就是:

模块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

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

相关推荐
德莱厄斯1 分钟前
没开玩笑,全框架支持的 dialog 组件,支持响应式
前端·javascript·github
非凡ghost29 分钟前
Affinity Photo(图像编辑软件) 多语便携版
前端·javascript·后端
非凡ghost31 分钟前
VideoProc Converter AI(视频转换软件) 多语便携版
前端·javascript·后端
endlesskiller37 分钟前
3年前我不会实现的,现在靠ai辅助实现了
前端·javascript
用户9047066835739 分钟前
commonjs的本质
前端
Sailing1 小时前
5分钟搞定 DeepSeek API 配置:从配置到调用一步到位
前端·openai·ai编程
Pa2sw0rd丶1 小时前
如何在 React 中实现键盘快捷键管理器以提升用户体验
前端·react.js
非凡ghost1 小时前
ToDoList(开源待办事项列表) 中文绿色版
前端·javascript·后端
j七七1 小时前
5分钟搭微信自动回复机器人5分钟搭微信自动回复机器人
运维·服务器·开发语言·前端·python·微信
快起来别睡了1 小时前
TypeScript装饰器详解:像搭积木一样给代码加功能
前端·typescript