移动端左滑删除功能的实现原理与React实战细节

A. 玉哲 交互 阅读 2,762
赞 10 收藏
二维码
手机扫码查看
反馈

项目初期的技术选型

上个月在做一个内部用的工单管理页,列表项需要支持快速删单。产品经理说“别点进详情再删,太慢,左滑直接删,像微信删聊天那样”。我第一反应是:又来了,这种交互看着简单,一写就掉坑里。

移动端左滑删除功能的实现原理与React实战细节

没多想,先否了第三方库——之前用过 vue-swipe-actions,iOS 上偶尔卡顿,Android 上 touch 事件冲突频发,维护方还半年没更新。这次决定手撸一个轻量、可控的左滑删除组件。目标很明确:不依赖框架、不引入新包、兼容 iOS/Android/Chrome DevTools 模拟器,且能和现有 Vue 3(Options API)和平共处。

最大的坑:touchmove 滚动失效

开始写得很顺:监听 touchstart 记坐标,touchmove 算位移,touchend 判断是否超过阈值(我设了 80px)。结果第一轮测试就翻车了——列表根本滚不动了。

查了半天,发现是 touchmove 里我默认调了 event.preventDefault(),本意是阻止默认滑动以保证拖拽手感,结果把整个页面滚动也干掉了。更尴尬的是,这个行为在 iOS Safari 上尤其顽固,{ passive: false } 还得手动加到 addEventListener 里,否则控制台直接报错。

后来改成只在“确认进入滑动态”后才 preventDefault:

let startX = 0;
let isDragging = false;

el.addEventListener('touchstart', (e) => {
  startX = e.touches[0].clientX;
  isDragging = false;
});

el.addEventListener('touchmove', (e) => {
  if (isDragging) {
    e.preventDefault(); // 只有真正在拖才阻止
    return;
  }
  const diffX = Math.abs(e.touches[0].clientX - startX);
  if (diffX > 10) {
    isDragging = true; // 超过 10px 才认定为拖拽,避免误触
  }
});

这里注意我踩过好几次坑:一开始用 Math.abs(diffX) > 0 就 trigger,结果手指刚碰屏就锁死滚动;后来试过 5px,还是太敏感;最终定在 10px,实测下来 Android 和 iOS 都挺稳。顺带一提,PC 端用 mousedown/mousemove 补充逻辑时,忘了禁用 selectstart,导致文字被误选,折腾了半天才发现……

核心代码就这几行

整个组件没封装成独立 Vue 组件,就塞在列表项的 <li> 里,够用就行。关键逻辑就三个状态:默认、滑出中、已滑出。用 CSS transform 控制位移,transition 做缓动,删操作走 API。

<li class="swipe-item" @touchstart="onTouchStart" @touchmove="onTouchMove" @touchend="onTouchEnd">
  <div class="content">工单 #12345</div>
  <div class="actions">
    <button @click.stop="deleteItem">删除</button>
  </div>
</li>
.swipe-item {
  position: relative;
  overflow: hidden;
  transition: transform 0.25s cubic-bezier(0.34, 1.56, 0.64, 1);
}

.swipe-item.swiped {
  transform: translateX(-80px);
}

.swipe-item .actions {
  position: absolute;
  right: 0;
  top: 0;
  width: 80px;
  height: 100%;
  background: #e74c3c;
  display: flex;
  align-items: center;
  justify-content: center;
}
export default {
  data() {
    return {
      isSwiping: false,
      startX: 0,
      currentX: 0
    }
  },
  methods: {
    onTouchStart(e) {
      this.startX = e.touches[0].clientX;
      this.isSwiping = false;
      this.currentX = 0;
    },
    onTouchMove(e) {
      if (!this.isSwiping) {
        const diffX = e.touches[0].clientX - this.startX;
        if (Math.abs(diffX) > 10) {
          this.isSwiping = true;
        }
      }
      if (this.isSwiping) {
        e.preventDefault();
        this.currentX = e.touches[0].clientX - this.startX;
        this.$el.style.transform = translateX(${Math.max(-80, this.currentX)}px);
      }
    },
    onTouchEnd(e) {
      if (!this.isSwiping) return;

      const finalX = this.currentX;
      const shouldDelete = finalX < -40; // 滑出一半即触发

      if (shouldDelete) {
        this.$el.classList.add('swiped');
        setTimeout(() => {
          this.deleteItem();
        }, 250); // 等动画结束再删
      } else {
        this.$el.style.transform = 'translateX(0)';
      }
      this.isSwiping = false;
      this.currentX = 0;
    },
    deleteItem() {
      // 实际调用接口
      fetch('https://jztheme.com/api/tickets/12345', { method: 'DELETE' })
        .then(() => {
          this.$emit('item-deleted', this.item.id);
        });
    }
  }
}

谁更灵活?谁更省事?

其实我也试过 CSS-only 方案(用 :active + transform),但 iOS 上 click 延迟太明显,体验差;还试过监听 scroll 来动态关闭滑动态,结果和下拉刷新库打架,放弃。

最后这套手撸方案最实在:逻辑清晰、调试方便、出问题一眼定位。唯一不完美的地方是——当快速连续滑两个 item,第二个会卡住半截。查了下是 transform 动画没清干净,加了个 getComputedStyle 强制重排就解决了,但我觉得影响不大,用户不会那么疯滑,就没深究。

踩坑提醒:这三点一定注意

  • passive 选项必须显式声明:iOS Safari 对 passive 默认为 true 的 touch 事件非常严格,不加 { passive: false } 会导致 preventDefault 失效或报错
  • 滑动阈值宁大勿小:10px 是底线,低于这个手指轻微抖动就会误判,尤其在横屏手持设备上
  • 删除后记得 emit 事件而非直接 DOM 移除:Vue 的响应式更新要靠数据驱动,直接 removeChild 会导致后续渲染异常

回顾与反思

做完回头一看,其实没必要追求“完美通用组件”。这个项目里就 3 个列表要用左滑删,硬套抽象层反而增加维护成本。现在这套代码放在 components/ListItem.vue 里,不到 80 行 JS,CSS 20 行,上线两周没报过滑动相关 bug。

当然还有优化空间:比如加个“向左滑显示更多操作”(编辑/标记),或者支持长按唤出菜单作为降级方案。但当前阶段,它就是够用、稳定、不拖累打包体积的解决方案。

以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

本文章不代表JZTHEME立场,仅为作者个人观点 / 研究心得 / 经验分享,旨在交流探讨,供读者参考。
发表评论

暂无评论