移动端 WebView 点击穿透问题解决方案

这两天 cursor 出了自己的模型 composer 1,速度是真的快,这篇就是它自己写,我稍微真理脱敏来的。

引言

在移动端 WebView 开发中,页面跳转时可能出现点击穿透(Click Through),导致误触底层元素。本文介绍一种在 Angular 中解决该问题的方案。

问题背景

典型场景:

  • 用户点击列表项
  • 代码执行路由跳转
  • 页面切换期间(约 200-300ms),用户可能继续点击
  • 点击可能穿透到新页面或底层元素

根本原因:

  • WebView 中 DOM 更新存在延迟
  • 页面切换期间事件仍可能被触发
  • 300ms 延迟可能进一步放大问题

解决方案实现

以下是完整代码实现:

/**
 * 列表项点击处理 - 跳转到详情页
 */
onItemClick(event: any, item: ListItem) {
  // 保存当前状态
  this.saveCurrentState();
  
  // 获取原始事件对象
  const src = event.srcEvent as any;

  // 步骤1: 阻止原始事件的默认行为和冒泡
  if (typeof (src as any).preventDefault === 'function') {
    (src as any).preventDefault();
  }

  if (typeof (src as any).stopPropagation === 'function') {
    (src as any).stopPropagation();
  }

  // 步骤2: 创建一个"杀手"函数,用于拦截后续的点击事件
  const killer = (e: Event) => {
    e.stopImmediatePropagation();  // 立即停止事件传播(比 stopPropagation 更彻底)
    e.preventDefault();            // 阻止默认行为
  };

  // 步骤3: 在 body 上添加捕获阶段的监听器
  const root = document.querySelector('body');
  if (root) {
    root.addEventListener('click', killer, { capture: true, once: true });
  }

  // 步骤4: 250ms 后移除监听器(click through avoid)
  setTimeout(() => {
    if (root) {
      root.removeEventListener('click', killer, true);
    }
  }, 250);

  // 执行页面跳转逻辑
  this.navigateToDetail(item.id);
}

核心代码解析

步骤1: 阻止原始事件传播

if (typeof (src as any).preventDefault === 'function') {
  (src as any).preventDefault();
}

if (typeof (src as any).stopPropagation === 'function') {
  (src as any).stopPropagation();
}

说明:

  • 阻止默认行为(如链接跳转)
  • 阻止事件冒泡
  • 使用类型检查避免调用不存在的方法

步骤2: 创建拦截函数

const killer = (e: Event) => {
  e.stopImmediatePropagation();  // 立即停止事件传播
  e.preventDefault();            // 阻止默认行为
};

说明:

  • stopImmediatePropagation() 会阻止同一元素上的其他监听器
  • preventDefault() 阻止默认行为

步骤3: 在捕获阶段拦截

const root = document.querySelector('body');
if (root) {
  root.addEventListener('click', killer, { capture: true, once: true });
}

说明:

  • capture: true:在捕获阶段拦截,优先于目标元素
  • once: true:自动移除(额外的手动移除作为保险)

步骤4: 定时清理

setTimeout(() => {
  if (root) {
    root.removeEventListener('click', killer, true);
  }
}, 250);

说明:

  • 250ms 后移除拦截器
  • 覆盖页面跳转的过渡期,避免长期阻塞交互

技术要点

1. 捕获阶段拦截

{ capture: true }

事件流顺序:

  1. 捕获阶段(Capture Phase)
  2. 目标阶段(Target Phase)
  3. 冒泡阶段(Bubble Phase)

在捕获阶段拦截可在事件到达目标前处理。

2. stopImmediatePropagation vs stopPropagation

  • stopPropagation():阻止事件继续传播
  • stopImmediatePropagation():立即停止,并阻止同一元素上的其他监听器

3. 时间窗口选择

250ms 覆盖:

  • 页面跳转动画(约 200-300ms)
  • 路由切换延迟
  • 避免误触又不影响正常交互

通用化封装

封装为可复用的工具函数:

/**
 * 防止点击穿透的工具函数
 * @param originalEvent 原始事件对象(可选)
 * @param timeout 拦截时长(毫秒),默认 250ms
 */
export function preventClickThrough(
  originalEvent?: any, 
  timeout: number = 250
): void {
  // 阻止原始事件
  if (originalEvent) {
    if (typeof originalEvent.preventDefault === 'function') {
      originalEvent.preventDefault();
    }
    if (typeof originalEvent.stopPropagation === 'function') {
      originalEvent.stopPropagation();
    }
  }

  // 创建拦截函数
  const killer = (e: Event) => {
    e.stopImmediatePropagation();
    e.preventDefault();
  };

  // 在 body 上添加捕获阶段监听
  const root = document.querySelector('body');
  if (root) {
    root.addEventListener('click', killer, { capture: true, once: true });

    // 定时清理
    setTimeout(() => {
      root.removeEventListener('click', killer, true);
    }, timeout);
  }
}

使用示例:

// Angular Component 中使用
onItemClick(event: any, item: ListItem) {
  // 防止点击穿透
  preventClickThrough(event.srcEvent);
  
  // 执行页面跳转
  this.router.navigate(['/detail', item.id]);
}

// 或者不传事件,只拦截后续点击
onButtonClick() {
  preventClickThrough();
  this.doSomething();
}

完整示例:Angular 组件

import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { preventClickThrough } from './utils/click-through-preventer';

interface ListItem {
  id: string;
  name: string;
  // ... 其他属性
}

@Component({
  selector: 'app-list',
  template: `
    <div *ngFor="let item of items" (click)="onItemClick($event, item)">
      {{ item.name }}
    </div>
  `
})
export class ListComponent {
  items: ListItem[] = [];

  constructor(private router: Router) {}

  onItemClick(event: any, item: ListItem): void {
    // 防止点击穿透
    preventClickThrough(event.srcEvent);
    
    // 执行导航
    this.router.navigate(['/detail', item.id]);
  }
}

适用场景

  • 列表项点击跳转详情页
  • 按钮点击触发路由导航
  • 模态框关闭后可能误触底层元素
  • 快速点击可能导致重复触发

注意事项

  1. 拦截时长应根据实际跳转延迟调整
  2. 确保在清理时正确移除监听器,避免内存泄漏
  3. 仅在必要时使用,避免影响正常交互
  4. 测试不同设备和浏览器的表现

总结

通过以下组合可有效防止点击穿透:

  • 阻止原始事件传播
  • 在捕获阶段拦截后续点击
  • 定时清理拦截器

该方案简单有效,适用于移动端 WebView 环境。建议封装为通用工具函数,便于项目中复用。