React智能弹窗队列深度解析:优先级控制与工程化实践全攻略

27 阅读2分钟

基于React实现智能弹窗队列的完整方案,支持优先级控制、显示频率限制和动态内容渲染,结合了队列管理、状态解耦和动画控制的核心思想:

一、架构设计

1. 核心数据结构

// types/modal.d.ts
export interface ModalConfig {
  id: string;             // 唯一标识
  content: React.ReactNode; // 弹窗内容
  priority?: number;      // 优先级(0-10)
  max displays?: number;  // 最大显示次数
  expireTime?: number;    // 过期时间戳
  onShow?: () => void;    // 显示回调
  onClose?: () => void;   // 关闭回调
}

2. 队列管理器实现

// hooks/useModalQueue.ts
import { useState, useCallback, useEffect } from 'react';
import { createRoot } from 'react-dom/client';

const MODAL_ROOT_ID = '__modal-root__';
const modalRoot = document.createElement('div');
modalRoot.id = MODAL_ROOT_ID;
document.body.appendChild(modalRoot);

const useModalQueue = () => {
  const [queue, setQueue] = useState<ModalConfig[]>([]);
  const [current, setCurrent] = useState<ModalConfig | null>(null);

  // 添加弹窗到队列
  const enqueue = useCallback((config: ModalConfig) => {
    // 频率控制逻辑
    const now = Date.now();
    const existing = queue.find(m => m.id === config.id);
    
    if (existing) {
      if (existing.maxDisplays && existing.displayCount >= existing.maxDisplays) {
        console.warn(`弹窗 ${config.id} 已达最大显示次数`);
        return;
      }
      
      if (existing.expireTime && now > existing.expireTime) {
        console.warn(`弹窗 ${config.id} 已过期`);
        return;
      }
      
      // 更新现有弹窗配置
      setQueue(prev => prev.map(m => 
        m.id === config.id ? { ...m, ...config } : m
      ));
    } else {
      setQueue(prev => [...prev, config]);
    }
  }, [queue]);

  // 处理队列逻辑
  const processQueue = useCallback(() => {
    if (!current && queue.length > 0) {
      const next = queue.sort((a, b) => b.priority! - a.priority!)[0];
      setCurrent(next);
    }
  }, [current, queue]);

  // 显示弹窗
  const showModal = useCallback((config: ModalConfig) => {
    enqueue(config);
    processQueue();
  }, [enqueue, processQueue]);

  // 关闭当前弹窗
  const closeModal = useCallback(() => {
    if (!current) return;
    
    current.onClose?.();
    setQueue(prev => prev.filter(m => m.id !== current.id));
    setCurrent(null);
    processQueue();
  }, [current, processQueue]);

  // 自动处理队列
  useEffect(() => {
    processQueue();
  }, [queue, processQueue]);

  // 渲染弹窗
  useEffect(() => {
    if (!current) return;
    
    const root = createRoot(modalRoot);
    root.render(
      <ModalWrapper 
        config={current}
        onClose={closeModal}
      />
    );
  }, [current, closeModal]);

  return { showModal, closeModal };
};

// 弹窗包装组件
const ModalWrapper = ({ config, onClose }: { 
  config: ModalConfig; 
  onClose: () => void 
}) => {
  return (
    <div className="modal-backdrop">
      <div className="modal-content">
        {config.content}
        <button onClick={onClose}>关闭</button>
      </div>
    </div>
  );
};

二、核心功能实现

1. 显示频率控制

// 示例:每天最多显示3次
const today = new Date().toISOString().split('T')[0];
const config: ModalConfig = {
  id: 'promotion',
  content: <PromotionComponent />,
  maxDisplays: 3,
  expireTime: Date.now() + 24 * 60 * 60 * 1000,
  onShow: () => {
    localStorage.setItem(`modal_${config.id}`, today);
  }
};

2. 优先级队列排序

// 在processQueue中修改排序逻辑
const next = queue.sort((a, b) => {
  if (a.priority === b.priority) return 0;
  return b.priority - a.priority; // 降序排列
})[0] || queue[0];

3. 动态内容渲染

// 使用动态组件
const DynamicContent = ({ type }: { type: 'alert' | 'confirm' }) => {
  return type === 'alert' ? <AlertComponent /> : <ConfirmComponent />;
};

// 调用示例
showModal({
  id: 'dynamic-modal',
  content: <DynamicContent type="confirm" />,
  priority: 5
});

三、高级特性扩展

1. 插队机制

// 修改enqueue方法
const enqueue = useCallback((config: ModalConfig & { immediate?: boolean }) => {
  if (config.immediate) {
    setQueue([config, ...queue.filter(m => m.id !== config.id)]);
  } else {
    // 常规入队逻辑
  }
}, [queue]);

2. 动画控制

// 使用react-transition-group
import { CSSTransition } from 'react-transition-group';

const ModalWrapper = ({ config, onClose }) => {
  return (
    <CSSTransition
      in={!!current}
      timeout={300}
      classNames="modal"
      unmountOnExit
    >
      <div className="modal-container">
        {config.content}
      </div>
    </CSSTransition>
  );
};

四、使用示例

// 注册弹窗
const { showModal } = useModalQueue();

// 显示普通弹窗
showModal({
  id: 'welcome',
  content: <WelcomeMessage />,
  priority: 3,
  maxDisplays: 1
});

// 显示高优先级弹窗
showModal({
  id: 'urgent-alert',
  content: <UrgentAlert />,
  priority: 10,
  immediate: true
});

五、状态持久化方案

// 使用localStorage记录显示历史
const useModalQueue = () => {
  const [queue, setQueue] = useState<ModalConfig[]>(() => {
    const stored = localStorage.getItem('modalQueue');
    return stored ? JSON.parse(stored) : [];
  });

  useEffect(() => {
    localStorage.setItem('modalQueue', JSON.stringify(queue));
  }, [queue]);
};

六、埋点统计增强

1. 埋点事件类型定义

// types/tracking.ts
export enum TrackingEventType {
  ModalShow = 'modal_show',
  ModalClose = 'modal_close',
  ButtonClick = 'modal_button_click',
  NetworkError = 'modal_network_error'
}

2. 埋点中间件实现

// hooks/useTrackingMiddleware.ts
import { useEffect } from 'react';
import { TrackingEventType } from '../types/tracking';

type TrackingMiddleware = (
  event: TrackingEventType,
  payload: Record<string, any>
) => void;

const trackingMiddlewares: TrackingMiddleware[] = [];

export const useTracking = () => {
  const track = (event: TrackingEventType, payload: Record<string, any>) => {
    // 执行所有中间件
    trackingMiddlewares.forEach(middleware => middleware(event, payload));
  };

  return { track };
};

// 注册全局埋点中间件(示例:发送到Google Analytics)
export const registerTrackingMiddleware = (middleware: TrackingMiddleware) => {
  trackingMiddlewares.push(middleware);
};

3. 弹窗埋点集成

// hooks/useModalQueue.ts
const useModalQueue = () => {
  const { track } = useTracking();

  // 显示弹窗时触发埋点
  const showModal = useCallback((config: ModalConfig) => {
    track(TrackingEventType.ModalShow, {
      modalId: config.id,
      source: config.source || 'system'
    });
    enqueue(config);
    processQueue();
  }, [track]);

  // 关闭弹窗埋点
  const closeModal = useCallback(() => {
    if (!current) return;
    track(TrackingEventType.ModalClose, { modalId: current.id });
    // ...原有关闭逻辑
  }, [current, track]);

  // 按钮点击埋点
  const trackButtonClick = useCallback((modalId: string, buttonType: 'confirm' | 'cancel') => {
    track(TrackingEventType.ButtonClick, { modalId, buttonType });
  }, [track]);
};

二、自动重试机制

1. 重试策略配置

// types/retry.ts
export interface RetryConfig {
  maxAttempts?: number;    // 最大重试次数
  initialDelay?: number;   // 初始延迟(ms)
  backoffFactor?: number;  // 退避系数(指数增长倍数)
}

2. 自动重试中间件

// hooks/useRetryMiddleware.ts
import { useEffect, useRef } from 'react';
import { TrackingEventType } from './tracking';

type RetryMiddleware = (
  action: () => Promise<void>,
  config: RetryConfig
) => Promise<void>;

export const useRetry = () => {
  const retryQueue = useRef<any[]>([]);

  const executeWithRetry = async <T>(
    action: () => Promise<T>,
    config: RetryConfig = {}
  ) => {
    const { maxAttempts = 3, initialDelay = 1000, backoffFactor = 2 } = config;
    let attempts = 0;

    while (attempts < maxAttempts) {
      try {
        return await action();
      } catch (error) {
        attempts++;
        if (attempts >= maxAttempts) throw error;

        const delay = initialDelay * Math.pow(backoffFactor, attempts);
        await new Promise(resolve => setTimeout(resolve, delay));
      }
    }
  };

  return { executeWithRetry };
};

3. 弹窗重试集成

// 在弹窗组件中使用
const PromotionModal = ({ onClose }: { onClose: () => void }) => {
  const { executeWithRetry } = useRetry();
  const track = useTracking();

  const handleShow = async () => {
    try {
      await executeWithRetry(
        () => sendTrackingData('modal_show', { modalId: 'promotion' }),
        { maxAttempts: 5, backoffFactor: 1.5 }
      );
    } catch (error) {
      track(TrackingEventType.NetworkError, {
        modalId: 'promotion',
        error: error.message
      });
    }
  };

  return (
    <div>
      <button onClick={() => handleShow()}>显示弹窗</button>
    </div>
  );
};

三、全局拦截器

1. 拦截器架构设计

// hooks/useInterceptor.ts
type Interceptor = (
  context: {
    modalId: string;
    config: ModalConfig;
    next: () => Promise<void>;
  }
) => Promise<void>;

const interceptors: Interceptor[] = [];

export const useInterceptor = () => {
  const register = (interceptor: Interceptor) => {
    interceptors.push(interceptor);
  };

  const runInterceptors = async (context: {
    modalId: string;
    config: ModalConfig;
    next: () => Promise<void>;
  }) => {
    for (const interceptor of interceptors) {
      await interceptor(context);
    }
  };

  return { register, runInterceptors };
};

2. 拦截器实现示例

// 权限拦截器
const authInterceptor = async ({ config, next }: InterceptorContext) => {
  if (config.requiresAuth && !isUserLoggedIn()) {
    redirectToLogin();
    throw new Error('权限拦截');
  }
  await next();
};

// 日志拦截器
const logInterceptor = async ({ config, next }: InterceptorContext) => {
  console.log(`[${new Date().toISOString()}] 弹窗显示: ${config.id}`);
  await next();
  console.log(`[${new Date().toISOString()}] 弹窗关闭: ${config.id}`);
};

3. 弹窗队列集成

// 修改processQueue逻辑
const processQueue = useCallback(async () => {
  if (!current && queue.length > 0) {
    const next = queue.sort(/*...*/)[0] || queue[0];
    
    try {
      await interceptor.runInterceptors({
        modalId: next.id,
        config: next,
        next: () => setCurrent(next)
      });
      setCurrent(next);
    } catch (error) {
      console.error('拦截器阻止弹窗显示:', error);
    }
  }
}, [interceptor]);

四、完整集成方案

1. 项目结构

src/
├── hooks/
│   ├── useModalQueue.ts       # 核心队列管理
│   ├── useTracking.ts         # 埋点中间件
│   ├── useRetry.ts            # 重试机制
│   └── useInterceptor.ts      # 拦截器系统
├── middleware/                # 中间件集合
│   ├── analytics.ts           # 埋点实现
│   ├── errorHandler.ts        # 错误处理
│   └── authInterceptor.ts     # 权限拦截
├── components/
│   └── ModalWrapper/          # 弹窗包装组件
└── utils/
    └── retryPolicy.ts         # 重试策略工具

2. 使用示例

// 初始化配置
const { track } = useTracking();
const { executeWithRetry } = useRetry();
const { register } = useInterceptor();

// 注册拦截器
register(authInterceptor);
register(logInterceptor);

// 显示带埋点的弹窗
const showModal = async () => {
  try {
    await executeWithRetry(
      () => showModalConfig({
        id: 'promotion',
        content: <PromotionComponent />,
        onShow: () => track('modal_show', { source: 'homepage' })
      }),
      { maxAttempts: 3 }
    );
  } catch (error) {
    track('modal_show_failed', { error: error.message });
  }
};