iframe 事件无法冒泡到父窗口的解决方案

背景介绍

问题场景

在现代 Web 应用中,iframe 被广泛用于嵌入第三方内容、文档预览、富文本编辑器等场景。然而,iframe 作为一个独立的文档上下文,其内部的事件无法直接冒泡到父窗口,这导致了一些问题:

  1. 事件监听失效 :父窗口无法监听到 iframe 内部的鼠标事件(如 mouseupmousedown
  2. 组件交互问题:某些依赖全局事件监听的组件(如拖拽组件、分割面板等)无法正常工作
  3. 用户体验影响:用户在 iframe 内操作时,父窗口的某些功能无法响应

实际案例

在聊天抽屉组件中,当内容区域包含 iframe(如 PDF 预览、Office 文档预览)时,用户在 iframe 内松开鼠标后,父窗口的 mouseup 事件监听器无法触发,导致依赖该事件的组件(如 splitpanes)无法正常工作。


解决思路

核心思路

将 iframe 内部的事件"转发"到父窗口,通过创建合成事件(Synthetic Event)使其能够在父窗口正常冒泡。

技术方案

1. 同源 iframe(推荐方案)

对于同源 iframe,可以直接访问其 contentDocument,在 iframe 内部添加事件监听器:

typescript 复制代码
// 在 iframe 内部添加事件监听
iframe.contentDocument.addEventListener('mouseup', (e) => {
  // 通过 postMessage 发送事件数据到父窗口
  window.parent.postMessage({
    type: 'mouseup',
    clientX: e.clientX,
    // ... 其他事件属性
  }, '*');
});

2. 跨域 iframe(备选方案)

对于跨域 iframe,由于浏览器的同源策略限制,无法访问其 contentDocument。需要在 iframe 内部主动发送 postMessage

typescript 复制代码
// iframe 内部代码
document.addEventListener('mouseup', (e) => {
  window.parent.postMessage({
    type: 'mouseup',
    // ... 事件数据
  }, '*');
});

3. 父窗口处理

父窗口监听 postMessage,接收事件数据后创建合成事件并派发:

typescript 复制代码
window.addEventListener('message', (event) => {
  const syntheticEvent = new MouseEvent('mouseup', {
    bubbles: true,
    clientX: convertedX, // 转换后的坐标
    // ... 其他属性
  });
  document.dispatchEvent(syntheticEvent); // 派发到 document,使其能够冒泡
});

坐标转换

iframe 内部的坐标是相对于 iframe 的,需要转换为相对于主页面的坐标:

typescript 复制代码
const rect = iframe.getBoundingClientRect();
const clientX = rect.left + iframeEvent.clientX;
const clientY = rect.top + iframeEvent.clientY;

组件封装

为了避免在每个需要 iframe 事件转发的组件中重复实现相同逻辑,我们将该功能封装成了 Vue Composable(useIframeEventForwarder)。

typescript 复制代码
import { onMounted, onBeforeUnmount, Ref, nextTick, watchEffect, watch } from "vue";

/**
 * iframe 事件转发配置选项
 */
export interface IframeEventForwarderOptions {
  /**
   * 要转发的事件类型列表,默认为 ['mouseup']
   */
  eventTypes?: string[];

  /**
   * 是否自动在 iframe 内部添加事件监听
   * - 同源 iframe:可以自动添加监听器(默认为 true)
   * - 跨域 iframe:无法自动添加,需要 iframe 内部主动发送 postMessage
   *   此时应设置为 false,并在 iframe 内部使用提供的脚本模板
   * 默认为 true
   */
  autoSetupIframeListeners?: boolean;

  /**
   * 延迟设置 iframe 监听器的时间(毫秒),默认为 1000
   */
  setupDelay?: number;

  /**
   * 是否监听 DOM 变化以动态添加新 iframe 的监听,默认为 true
   */
  watchDomChanges?: boolean;

  /**
   * 事件处理回调函数
   * @param eventType 事件类型
   * @param syntheticEvent 合成的事件对象
   */
  onEvent?: (eventType: string, syntheticEvent) => void;

  /**
   * 是否验证 postMessage 的 origin,提高安全性
   * @param origin 消息来源
   * @returns 是否允许该来源的消息
   */
  validateOrigin?: (origin: string) => boolean;

  /**
   * 限制监听的 iframe 范围(可选)
   * - 如果提供字符串,则作为 CSS 选择器使用
   * - 如果提供 HTMLElement 或 Ref<HTMLElement>,则只监听该元素内的 iframe
   * - 如果不提供,则监听页面中所有的 iframe
   */
  iframeSelector?: string | HTMLElement | Ref<HTMLElement | null>;
}

/**
 * 生成跨域 iframe 事件转发脚本
 * 如果 iframe 是跨域的,需要在 iframe 内部添加此脚本
 *
 * @param eventTypes 要转发的事件类型列表,默认为 ['mouseup']
 * @param targetOrigin postMessage 的目标 origin,默认为 '*'(允许所有来源)
 * @returns 可执行的 JavaScript 代码字符串
 *
 */
export function generateCrossOriginIframeScript(
  eventTypes: string[] = ['mouseup'],
  targetOrigin: string = '*'
): string {
  const eventTypesStr = JSON.stringify(eventTypes);
  return `
(function() {
  var eventTypes = ${eventTypesStr};

  eventTypes.forEach(function(eventType) {
    document.addEventListener(eventType, function(e) {
      var eventData = {
        type: eventType,
        source: 'iframe',
        screenX: e.screenX,
        screenY: e.screenY,
        clientX: e.clientX,
        clientY: e.clientY,
        button: e.button,
        buttons: e.buttons,
        ctrlKey: e.ctrlKey,
        shiftKey: e.shiftKey,
        altKey: e.altKey,
        metaKey: e.metaKey,
      };
      window.parent.postMessage(eventData, ${JSON.stringify(targetOrigin)});
    });
  });
})();
`.trim();
}

/**
 * 跨域 iframe 事件转发脚本模板(默认配置)
 * 使用默认事件类型 ['mouseup']
 */
export const CROSS_ORIGIN_IFRAME_SCRIPT = generateCrossOriginIframeScript();

/**
 * iframe 事件转发器
 * 用于将 iframe 内部的事件转发到父窗口,使其能够正常冒泡
 *
 * **同源 vs 跨域 iframe:**
 *
 * 1. **同源 iframe**(推荐):
 *    - 可以自动在 iframe 内部添加事件监听器
 *    - 无需 iframe 内部代码配合
 *    - 设置 `autoSetupIframeListeners: true`(默认)
 *
 * 2. **跨域 iframe**:
 *    - 由于浏览器安全策略,无法访问跨域 iframe 的 contentDocument
 *    - 需要 iframe 内部主动发送 postMessage
 *    - 设置 `autoSetupIframeListeners: false`
 *    - 在 iframe 内部使用 `CROSS_ORIGIN_IFRAME_SCRIPT` 脚本模板

 */
export function useIframeEventForwarder(
  options: IframeEventForwarderOptions = {}
) {
  const {
    eventTypes = ["mouseup"],
    autoSetupIframeListeners = true,
    setupDelay = 1000,
    watchDomChanges = true,
    onEvent,
    validateOrigin,
    iframeSelector,
  } = options;

  let messageHandler: ((event: MessageEvent) => void) | null = null;
  let observer: MutationObserver | null = null;
  const iframeEventHandlers = new Map<HTMLIFrameElement, Set<() => void>>();

  /**
   * 获取实际的容器元素
   */
  const getContainer = (): HTMLElement | null => {
    if (!iframeSelector) {
      return null;
    }
    if (typeof iframeSelector === "string") {
      return null; // 字符串选择器,不需要容器
    }
    // 如果是 Ref,获取其 value
    if ('value' in iframeSelector) {
      return iframeSelector.value;
    }
    // 如果是 HTMLElement,直接返回
    return iframeSelector;
  };

  /**
   * 获取要监听的 iframe 列表
   */
  const getIframes = (): NodeListOf<HTMLIFrameElement> => {
    const container = getContainer();
    if (container) {
      // 有容器,在容器内查找
      return container.querySelectorAll<HTMLIFrameElement>("iframe");
    }
    if (typeof iframeSelector === "string") {
      // 字符串选择器
      return document.querySelectorAll<HTMLIFrameElement>(iframeSelector);
    }
    // 没有限制,查找所有 iframe
    return document.querySelectorAll<HTMLIFrameElement>("iframe");
  };

  /**
   * 计算相对于主页面的坐标
   */
  const calculateCoordinates = (
    eventData: any,
    eventSource: Window | null
  ): { clientX: number; clientY: number; screenX: number; screenY: number } => {
    let clientX = eventData.clientX || 0;
    let clientY = eventData.clientY || 0;
    const screenX = eventData.screenX || 0;
    const screenY = eventData.screenY || 0;

    // 尝试找到发送消息的 iframe,并转换坐标
    if (eventSource) {
      try {
        const iframes = getIframes();

        for (const iframe of iframes) {
          if (iframe.contentWindow === eventSource) {
            // 找到对应的 iframe,计算相对于主页面的坐标
            const rect = iframe.getBoundingClientRect();
            clientX = rect.left + (eventData.clientX || 0);
            clientY = rect.top + (eventData.clientY || 0);
            break;
          }
        }
      } catch (error) {
        console.warn("[useIframeEventForwarder] 无法计算 iframe 坐标,使用原始值", error);
      }
    }

    return { clientX, clientY, screenX, screenY };
  };

  /**
   * 创建合成事件
   */
  const createSyntheticEvent = (
    eventType: string,
    eventData: any,
    eventSource: Window | null
  ): Event | null => {
    try {
      const coords = calculateCoordinates(eventData, eventSource);

      // 根据事件类型创建不同的事件对象
      if (eventType === "mouseup" || eventType === "mousedown" || eventType === "mousemove" || eventType === "click") {
        return new MouseEvent(eventType, {
          bubbles: true,
          cancelable: true,
          view: window,
          detail: eventData.detail || 0,
          screenX: coords.screenX,
          screenY: coords.screenY,
          clientX: coords.clientX,
          clientY: coords.clientY,
          button: eventData.button || 0,
          buttons: eventData.buttons || 0,
          ctrlKey: eventData.ctrlKey || false,
          shiftKey: eventData.shiftKey || false,
          altKey: eventData.altKey || false,
          metaKey: eventData.metaKey || false,
        });
      } else if (eventType === "keyup" || eventType === "keydown" || eventType === "keypress") {
        return new KeyboardEvent(eventType, {
          bubbles: true,
          cancelable: true,
          view: window,
          key: eventData.key || "",
          code: eventData.code || "",
          ctrlKey: eventData.ctrlKey || false,
          shiftKey: eventData.shiftKey || false,
          altKey: eventData.altKey || false,
          metaKey: eventData.metaKey || false,
        });
      } else {
        // 对于其他事件类型,创建通用事件
        return new Event(eventType, {
          bubbles: true,
          cancelable: true,
        });
      }
    } catch (error) {
      console.error(`[useIframeEventForwarder] 创建合成事件失败 (${eventType}):`, error);
      return null;
    }
  };

  /**
   * 处理来自 iframe 的 postMessage
   */
  const handleIframeMessage = (event: MessageEvent) => {
    // 验证 origin(如果提供了验证函数)
    if (validateOrigin && !validateOrigin(event.origin)) {
      return;
    }

    const eventData = event.data;
    if (!eventData || !eventData.type) {
      return;
    }

    // 检查是否是我们要转发的事件类型
    if (!eventTypes.includes(eventData.type)) {
      return;
    }

    // 创建合成事件
    const syntheticEvent = createSyntheticEvent(
      eventData.type,
      eventData,
      event.source as Window | null
    );

    if (!syntheticEvent) {
      return;
    }

    // 派发事件到 document,让事件能够冒泡到 window
    document.dispatchEvent(syntheticEvent);

    // 调用用户提供的回调
    if (onEvent) {
      onEvent(eventData.type, syntheticEvent);
    }
  };

  /**
   * 检测 iframe 是否跨域
   * 跨域时无法访问 contentDocument,会抛出异常
   */
  const isCrossOrigin = (iframe: HTMLIFrameElement): boolean => {
    try {
      // 尝试访问 contentDocument,跨域时会抛出异常
      const doc = iframe.contentDocument;
      return doc === null;
    } catch (error) {
      // 跨域访问会抛出 SecurityError
      return true;
    }
  };

  /**
   * 在 iframe 内部添加事件监听
   * - 同源 iframe:可以直接访问并添加监听器
   * - 跨域 iframe:无法访问,需要 iframe 内部主动发送 postMessage
   */
  const setupIframeListeners = () => {
    const iframes = getIframes();
    iframes.forEach((iframe) => {
      // 如果已经设置过监听器,跳过
      if (iframeEventHandlers.has(iframe)) {
        return;
      }

      const handlers = new Set<() => void>();

      // 检测是否跨域
      if (isCrossOrigin(iframe)) {
        // 跨域 iframe:无法直接访问,需要 iframe 内部主动发送 postMessage
        console.warn(
          `[useIframeEventForwarder] 检测到跨域 iframe,无法自动添加事件监听器。` +
          `请设置 autoSetupIframeListeners: false,并在 iframe 内部使用 CROSS_ORIGIN_IFRAME_SCRIPT 脚本模板。`
        );
        return;
      }

      try {
        // 检查 iframe 是否已加载
        if (iframe.contentWindow && iframe.contentDocument) {
          // 同源 iframe:可以直接访问并添加监听器
          // 延迟一点时间,确保 iframe 内容完全加载
          setTimeout(() => {
            setupIframeDocumentListeners(iframe, handlers);
          }, 100);
        } else {
          // iframe 未加载完成,等待加载
          const loadHandler = () => {
            try {
              // 再次检测是否跨域(加载后可能变成跨域)
              if (isCrossOrigin(iframe)) {
                console.warn(
                  `[useIframeEventForwarder] iframe 加载后检测为跨域,无法添加事件监听器。`
                );
                return;
              }
              if (iframe.contentDocument) {
                // 延迟一点时间,确保 iframe 内容完全加载
                setTimeout(() => {
                  setupIframeDocumentListeners(iframe, handlers);
                }, 100);
              }
            } catch (error) {
              console.warn(
                "[useIframeEventForwarder] 无法访问 iframe 内容(可能是跨域)",
                error
              );
            }
          };
          iframe.addEventListener("load", loadHandler);
          handlers.add(() => {
            iframe.removeEventListener("load", loadHandler);
          });
        }
      } catch (error) {
        console.warn(
          "[useIframeEventForwarder] 无法访问 iframe 内容(可能是跨域)",
          error
        );
      }

      if (handlers.size > 0) {
        iframeEventHandlers.set(iframe, handlers);
      }
    });
  };

  /**
   * 在 iframe 的 document 和 window 上设置事件监听
   */
  const setupIframeDocumentListeners = (
    iframe: HTMLIFrameElement,
    handlers: Set<() => void>
  ) => {
    const iframeDoc = iframe.contentDocument;
    if (!iframeDoc) {
      console.warn("[useIframeEventForwarder] iframe document 或 window 不可用");
      return;
    }

    // 创建事件处理函数
    const createEventHandler = (eventType: string) => {
      return (e: Event) => {
        const mouseEvent = e as MouseEvent;
        const keyboardEvent = e as KeyboardEvent;

        // 构建要发送的事件数据
        const eventData: any = {
          type: eventType,
          source: "iframe",
        };

        // 如果是鼠标事件,添加鼠标相关属性
        if (mouseEvent instanceof MouseEvent) {
          eventData.screenX = mouseEvent.screenX;
          eventData.screenY = mouseEvent.screenY;
          eventData.clientX = mouseEvent.clientX;
          eventData.clientY = mouseEvent.clientY;
          eventData.button = mouseEvent.button;
          eventData.buttons = mouseEvent.buttons;
          eventData.ctrlKey = mouseEvent.ctrlKey;
          eventData.shiftKey = mouseEvent.shiftKey;
          eventData.altKey = mouseEvent.altKey;
          eventData.metaKey = mouseEvent.metaKey;
        }

        // 如果是键盘事件,添加键盘相关属性
        if (keyboardEvent instanceof KeyboardEvent) {
          eventData.key = keyboardEvent.key;
          eventData.code = keyboardEvent.code;
          eventData.ctrlKey = keyboardEvent.ctrlKey;
          eventData.shiftKey = keyboardEvent.shiftKey;
          eventData.altKey = keyboardEvent.altKey;
          eventData.metaKey = keyboardEvent.metaKey;
        }

        // 发送消息到父窗口
        if (iframe.contentWindow) {
          iframe.contentWindow.parent.postMessage(eventData, "*");
        }
      };
    };

    eventTypes.forEach((eventType) => {
      const handler = createEventHandler(eventType);

      iframeDoc.addEventListener(eventType, handler);

      handlers.add(() => {
        iframeDoc.removeEventListener(eventType, handler);
      });
    });
  };

  /**
   * 清理 iframe 监听器
   */
  const cleanupIframeListeners = () => {
    iframeEventHandlers.forEach((handlers, iframe) => {
      handlers.forEach((cleanup) => cleanup());
    });
    iframeEventHandlers.clear();
  };

  /**
   * 初始化 postMessage 监听(只需要初始化一次)
   */
  const initMessageListener = () => {
    if (messageHandler) {
      return;
    }
    messageHandler = handleIframeMessage;
    window.addEventListener("message", messageHandler);
  };

  /**
   * 设置 DOM 变化监听器
   */
  const setupMutationObserver = () => {
    // 如果已经有 observer,先清理
    if (observer) {
      observer.disconnect();
      observer = null;
    }

    if (!watchDomChanges) {
      return;
    }

    const container = getContainer();
    const targetContainer = container || document.body;

    observer = new MutationObserver(() => {
      setupIframeListeners();
    });
    observer.observe(targetContainer, {
      childList: true,
      subtree: true,
    });
  };

  // 初始化 postMessage 监听(只需要一次)
  initMessageListener();

  // 使用 watchEffect 自动监听容器和 iframe 的变化
  watchEffect(() => {
    // 访问 iframeSelector(如果是 Ref,watchEffect 会自动追踪其 value 的变化)
    let container: HTMLElement | null = null;
    if (iframeSelector) {
      if (typeof iframeSelector === "string") {
        // 字符串选择器,不需要容器
      } else if ('value' in iframeSelector) {
        // Ref,访问 value 以触发响应式追踪
        container = iframeSelector.value;
      } else {
        // HTMLElement
        container = iframeSelector;
      }
    }

    // 如果容器存在或者是字符串选择器,设置监听器
    if (container || typeof iframeSelector === "string" || !iframeSelector) {
      if (autoSetupIframeListeners) {
        // 延迟执行,确保 DOM 已更新
        setTimeout(() => {
          setupIframeListeners();
        }, setupDelay);
      }

      // 设置 DOM 变化监听
      setupMutationObserver();
    }
  });

  // 组件卸载时自动清理
  onBeforeUnmount(() => {
    if (messageHandler) {
      window.removeEventListener("message", messageHandler);
      messageHandler = null;
    }
    if (observer) {
      observer.disconnect();
      observer = null;
    }
    cleanupIframeListeners();
  });

  return {
    /**
     * 手动设置 iframe 监听器
     */
    setupListeners: setupIframeListeners,

    /**
     * 清理所有监听器(通常不需要手动调用,组件卸载时会自动清理)
     */
    cleanup: () => {
      if (messageHandler) {
        window.removeEventListener("message", messageHandler);
        messageHandler = null;
      }
      if (observer) {
        observer.disconnect();
        observer = null;
      }
      cleanupIframeListeners();
    },
  };
}

核心功能

主要特性

  1. 自动事件转发:自动将 iframe 内部的事件转发到父窗口
  2. 坐标自动转换:自动将 iframe 坐标转换为父窗口坐标
  3. 支持多种事件类型:支持鼠标事件、键盘事件等
  4. 自动清理:组件卸载时自动清理所有监听器
  5. 动态监听:自动监听 DOM 变化,为新添加的 iframe 设置监听
  6. 作用域限制:支持限制监听范围,只监听指定容器内的 iframe

支持的事件类型

  • 鼠标事件mouseupmousedownmousemoveclick
  • 键盘事件keyupkeydownkeypress
  • 其他事件:支持任意事件类型(通过通用 Event 创建)

使用方法

基础用法

typescript 复制代码
<script setup>
import { ref } from "vue";
import { useIframeEventForwarder } from "@/hooks/useIframeEventForwarder";

const containerRef = ref<HTMLElement | null>(null);

// 最简单的用法:转发 mouseup 事件
useIframeEventForwarder({
  iframeSelector: containerRef, // 只监听组件内的 iframe
});
</script>
<template>
  <div ref="containerRef">
    <iframe src="..."></iframe>
  </div>
</template>

完整配置

typescript 复制代码
<script setup>
import { ref } from "vue";
import { useIframeEventForwarder } from "@/hooks/useIframeEventForwarder";

const containerRef = ref<HTMLElement | null>(null);

useIframeEventForwarder({
  // 要转发的事件类型
  eventTypes: ["mouseup", "mousedown", "click"],
  
  // 限制监听范围(传入 ref)
  iframeSelector: containerRef,
  
  // 事件处理回调
  onEvent: (eventType, syntheticEvent) => {
    console.log(`收到 iframe 的 ${eventType} 事件`, syntheticEvent);
  },
  
  // 验证消息来源(跨域场景)
  validateOrigin: (origin) => {
    return origin === "https://trusted-domain.com";
  },
  
  // 是否自动设置监听器(同源 iframe 默认为 true)
  autoSetupIframeListeners: true,
  
  // 延迟设置时间(毫秒)
  setupDelay: 1000,
  
  // 是否监听 DOM 变化
  watchDomChanges: true,
});
</script>

跨域 iframe 使用

typescript 复制代码
<script setup>
import { useIframeEventForwarder, generateCrossOriginIframeScript } from "@/hooks/useIframeEventForwarder";

// 父窗口:只监听 postMessage
useIframeEventForwarder({
  eventTypes: ["mouseup"],
  autoSetupIframeListeners: false, // 跨域时设置为 false
  validateOrigin: (origin) => {
    return origin === "https://trusted-domain.com";
  },
});

// iframe 内部需要添加脚本(通过其他方式注入)
// const script = generateCrossOriginIframeScript(['mouseup']);
</script>

最佳实践

1. 限制监听范围

推荐 :使用 iframeSelector 传入容器 ref,只监听组件内的 iframe

typescript 复制代码
// ✅ 推荐:只监听组件内的 iframe
const containerRef = ref<HTMLElement | null>(null);
useIframeEventForwarder({
  iframeSelector: containerRef,
});

// ❌ 不推荐:监听全局所有 iframe(可能影响性能)
useIframeEventForwarder({});

2. 只监听必要的事件

推荐:只监听实际需要的事件类型

typescript 复制代码
// ✅ 推荐:只监听需要的事件
useIframeEventForwarder({
  eventTypes: ["mouseup"], // 只监听 mouseup
});

// ❌ 不推荐:监听过多事件(影响性能)
useIframeEventForwarder({
  eventTypes: ["mouseup", "mousedown", "mousemove", "click", "keyup", "keydown"],
});

3. 跨域场景的安全验证

推荐:始终验证消息来源

typescript 复制代码
// ✅ 推荐:验证 origin
useIframeEventForwarder({
  validateOrigin: (origin) => {
    return origin === "https://trusted-domain.com";
  },
});

// ❌ 不推荐:不验证 origin(安全风险)
useIframeEventForwarder({
  // 没有 validateOrigin
});

4. 性能优化

推荐:根据实际需求调整配置

typescript 复制代码
// 如果不需要监听动态添加的 iframe
useIframeEventForwarder({
  watchDomChanges: false, // 关闭 DOM 变化监听
});

// 如果 iframe 加载很快,可以减少延迟
useIframeEventForwarder({
  setupDelay: 500, // 减少延迟时间
});

5. 组件内使用

推荐:在 setup 顶层调用,传入容器 ref

typescript 复制代码
<script setup>
import { ref } from "vue";
import { useIframeEventForwarder } from "@/hooks/useIframeEventForwarder";

const containerRef = ref<HTMLElement | null>(null);

// ✅ 推荐:在 setup 顶层调用
useIframeEventForwarder({
  iframeSelector: containerRef,
  eventTypes: ["mouseup"],
});

// composable 会自动:
// 1. 监听容器变化
// 2. 自动更新事件监听
// 3. 组件卸载时自动清理
</script>

注意事项

1. 同源策略限制

  • 同源 iframe:可以自动添加事件监听器,推荐使用
  • 跨域 iframe :无法访问 contentDocument,需要 iframe 内部配合

2. 性能考虑

  • 避免监听过多事件类型
  • 使用 iframeSelector 限制监听范围
  • 如果不需要动态监听,设置 watchDomChanges: false

3. 安全建议

  • 跨域场景务必使用 validateOrigin 验证消息来源
  • 不要在生产环境使用 "*" 作为 postMessage 的 targetOrigin
  • 验证事件数据的合法性

相关资源

相关推荐
用户6600676685392 小时前
纯 CSS 复刻星战开场:让文字在宇宙中滚动
前端·css
AAA简单玩转程序设计2 小时前
Java里的空指针
java·前端
时72 小时前
PDF.js 在 Vue 中的使用指南
前端
鹘一2 小时前
Prompts 组件实现
前端·javascript
大菜菜2 小时前
Molecule Framework - ExplorerService API 详细文档
前端
_一两风2 小时前
Vue-TodoList 项目详解
前端·javascript·vue.js
北辰alk2 小时前
Vue中mixin与mixins:全面解析与实战指南
前端·vue.js
脾气有点小暴2 小时前
UniApp实现刷新当前页面
开发语言·前端·javascript·vue.js·uni-app