移动端左滑删除功能的实现原理与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。
当然还有优化空间:比如加个“向左滑显示更多操作”(编辑/标记),或者支持长按唤出菜单作为降级方案。但当前阶段,它就是够用、稳定、不拖累打包体积的解决方案。
以上是我踩坑后的总结,希望对你有帮助。这个技巧的拓展用法还有很多,后续会继续分享这类博客。有更优的实现方式欢迎评论区交流。

暂无评论