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

前言

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

简单说明就是:

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

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

相关推荐
真滴book理喻17 分钟前
Vue(四)
前端·javascript·vue.js
蜜獾云19 分钟前
npm淘宝镜像
前端·npm·node.js
dz88i820 分钟前
修改npm镜像源
前端·npm·node.js
Jiaberrr24 分钟前
解锁 GitBook 的奥秘:从入门到精通之旅
前端·gitbook
顾平安2 小时前
Promise/A+ 规范 - 中文版本
前端
聚名网2 小时前
域名和服务器是什么?域名和服务器是什么关系?
服务器·前端
桃园码工2 小时前
4-Gin HTML 模板渲染 --[Gin 框架入门精讲与实战案例]
前端·html·gin·模板渲染
不是鱼2 小时前
构建React基础及理解与Vue的区别
前端·vue.js·react.js
沈剑心2 小时前
如何在鸿蒙系统上实现「沉浸式」页面?
前端·harmonyos
一棵开花的树,枝芽无限靠近你2 小时前
【PPTist】组件结构设计、主题切换
前端·笔记·学习·编辑器