Taro+react,单页面应用实践全局弹窗管理

1,409 阅读2分钟

我们都知道,在小程序里面是没有dom的概念的,那么之前基于react-dom使用的例如document.append实现的动态向body中添加modal节点的方法就是失效了。 相比之下,我们必须在小程序中提前定义好所有的弹窗层,在合适的时机将弹窗显示和隐藏并展示动画过程,如果写在业务递进的组件内部往往会因为各种原因导致弹窗的动画效果失败甚至弹窗被遮挡。

我贴一下我在之前的项目中自己用到的在最外层管理所有弹层的方法,以下写法可以把弹层写在最外侧的jsx中,自己通过mobx或者redux管理hook返回的各种状态。

const { triggerOpen, triggerClose, getModalConfig, zIndexs } = registerModal([
    "xxx1",
    "xxx2",
    "xxx3",
    "xxx4"
  ]);
...
// xxx1的弹窗:
// 最外层的open控制显隐,第二层的animate控制动画
<div 
 className={classnames("xxx1-modal", getModalConfig("xxx1").open ? 'show' : 'hidden')}
 style={{zIndex: zIndexs["xxx1"] || 2}}
 >
     <div className={classnames("xxx1-animate", getModalConfig("xxx1").animate ? 'open': 'close')}>
         <自定义的组件 onOpen={triggetOpen} onClose={() => triggerClose("xxx1")}/>
     </div>
 </div>

registerModal作为hook返回几个变量

  • triggerOpen:通过传入注册的弹窗名称(例如:xxx1,xxx2等),可以打开任意预定义的弹窗,不会重复打开同一个弹窗
  • triggetClose:通过传入注册的弹窗名称,可以关闭任意预定义的弹窗
  • getModalConfig:通过传入注册的弹窗名称,获取对应弹窗的open和animate两个状态,animate状态是hook内部处理好的,默认是300ms的执行时间
  • zIndexs:可以任意顺序注册,通过显式控制z-index来让弹窗不会相互覆盖,默认最后打开的弹窗优先级最高

下面给出registerModal的实现

export const registerModal = (names: string[]) => {
  const [open, setOpen] = useState<Record<string, boolean>>(getInit(names));
  const [animate, setAnimate] = useState<Record<string, boolean>>(
    getInit(names)
  );

  const [stack, setStack] = useState<string[]>([]);
  const [zIndexs, setZIndex] = useState<Record<string, number>>({});

  /**
   * 认为,新打开的层级永远高于之前打开的
   */
  useEffect(() => {
    const result = {};
    stack.forEach((s, index) => {
      result[s] = index + 2;
    });
    setZIndex({ ...result });
  }, [stack]);

  const getModalConfig = useCallback(
    (name: string) => {
      return {
        open: open[name],
        animate: animate[name],
      };
    },
    [open, animate]
  );

  const setOpenByName = useCallback((name: string, cb: any) => {
    setOpen((v) => {
      if (v[name] === undefined) {
        return v;
      } else {
        v[name] = typeof cb === "function" ? cb(v[name]) : cb;
        return { ...v };
      }
    });
  }, []);

  const setAnimateByName = useCallback((name: string, cb: any) => {
    setAnimate((v) => {
      if (v[name] === undefined) {
        return v;
      } else {
        v[name] = typeof cb === "function" ? cb(v[name]) : cb;
        return { ...v };
      }
    });
  }, []);

  const triggerOpen = useCallback(
    (name: string) => {
      const isOpen = open[name];
      if (isOpen !== undefined) {
        setStack((s) => {
          const temp = [...s];
          temp.push(name);
          return [...temp];
        });
        setOpenByName(name, true);
        setTimeout(() => {
          setAnimateByName(name, true);
        }, 10);
      }
    },
    [open]
  );

  const triggerClose = useCallback(
    (name: string) => {
      const isOpen = open[name];
      if (isOpen !== undefined) {
        setStack((s) => {
          const temp = [...s];
          const index = s.findIndex((v) => v === name);
          temp.splice(index, 1);
          return [...temp];
        });
        setAnimateByName(name, false);
        setTimeout(() => {
          setOpenByName(name, false);
        }, 300);
      }
    },
    [open]
  );

  return {
    getModalConfig,
    triggerOpen,
    triggerClose,
    zIndexs,
  };
};