资讯动态

从Canvas到PointerEvent:兼容PC与移动端的刮刮卡Vue组件封装指南

发布时间:2026/10/13 14:53:52 来源:尧图企业网站定制
简介面向需要在PC端和移动端实现刮刮卡交互效果的Vue开发者这份组件资源提供了一套可直接运行的刮刮卡方案覆盖从涂抹刮层到结果回调的完整流程无需从零搭建画布逻辑。压缩包共16个文件以5个JavaScript文件、2个Vue组件文件、2个JSON配置文件为核心另含打包后的产物、源码映射map、示例页面HTML及基础工程配置整体仅86KB便于快速集成与二次修改。目前已有1321人学习使用适合具备一定Vue基础、希望为活动页或营销场景增加趣味交互的开发者参考。通过源码可重点理解vue-scratch-card的组件封装方式、canvas刮层与移动端触摸事件的处理思路同时借助dist目录下的编译文件直接部署到现有项目中。1. 刮刮卡 Vue 组件为什么 PC 和移动端都要自己做而不是找插件在营销活动里刮刮卡是转化率一直很稳定的交互但要做成一个兼容 PC 和移动端的 Vue 组件远比放一张 gif 复杂。真正动手时才会发现PC 上鼠标刮得顺滑换到手机就变成“点一下才动一下”或者刮完底色花掉、画布模糊。与其在现成插件和业务之间反复改样式不如自己封装一个刮刮卡 Vue 组件同时兼顾两端。下面会从 Canvas 原理讲起给出一份能跑通的最小实现、事件归一化方案以及我在活动落地里踩过的坑。适合正在做营销活动、想要把刮刮卡变成可复用业务的开发者。2. 刮开效果的核心原理Canvas 混合模式与刮开判定如何选型要封装一个能在 PC 和移动端同时用的刮刮卡 Vue 组件第一步不是写工具函数而是先把刮开这个动作在浏览器里变成像素级的操作。我把常见实现梳理了一下最终能稳定工作的路线是 Canvas 2D 图层 擦除式合成模式 事件坐标映射。底层图片放在 canvas 下面canvas 覆盖一层涂层当用户滑动时把涂层的对应区域清成透明露出的就是奖品区。听起来简单但里面的混合模式、触摸事件和像素采样每个点都能出问题。2.1 为什么用 Canvas 而不是 CSS 遮罩或双层图片先对比一下另两种常见做法。有人会用 CSS 的 background-clip: text 或者 mask 做擦除但 CSS 遮罩只能按照固定图形或路径来裁剪没法跟随用户的鼠标轨迹实时生成不规则的擦除区域。也有人用上下两层 div上层做涂层手指滑过时给上层添加“已刮掉”的矩形块这种做法的颗粒感太强边缘也很生硬。Canvas 的优势在于它的每个像素都可以被独立控制。涂层是 canvas 上的一层半透明像素刮开就是在这些像素上做 alpha 通道的“减法”。无论是圆头线条、曲线还是不规则轨迹都能以像素为单位平滑呈现。对于刮刮卡这种交互2D Canvas 已经能完全覆盖需求WebGL 虽然也能做但还要管理着色器、纹理和上下文丢失业务侧用它属于杀鸡用牛刀。我一般建议组件基于原生 Canvas 2D 实现不引第三方图形库这样包体积最小也最容易排查问题。2.2 globalCompositeOperation 与刮开动作的像素级模拟Canvas 里负责“擦除”的核心属性是 globalCompositeOperation它决定新绘制的图形如何与已有内容合成。做刮刮卡时先把涂层画上去再把合成模式设置为 destination-out然后在涂层上画一条线或一个圆线条经过的地方会变成透明。destination-out 的含义是“保留目标图像中不需要绘制新图形覆盖的区域”翻译成大白话就是用画笔把原涂层挖掉。下面这段代码是刮开一笔的最小逻辑不经过 Vue直接在页面里验证const canvas document.querySelector(#scratchCard); const ctx canvas.getContext(2d, { alpha: true }); // 第一层涂层 ctx.fillStyle #c8c8c8; ctx.fillRect(0, 0, canvas.width, canvas.height); // 刮开一笔 ctx.save(); ctx.globalCompositeOperation destination-out; ctx.lineCap round; ctx.lineJoin round; ctx.lineWidth 24; ctx.beginPath(); ctx.moveTo(80, 60); ctx.lineTo(140, 100); ctx.stroke(); ctx.restore();这段代码先把整块 canvas 用灰色填充然后画一条结束点与起始点不同的线段线段覆盖区域的像素 alpha 会被清成 0。需要注意两点一是 save/restore 要成对出现保证后续的 fillStyle 不会和混合模式串掉二是在设置合成模式时最好显式创建带 alpha 的 context避免某些浏览器把背景默认成不透明黑色。实际拖动刮擦时应该用 stroke 一笔接一笔地画而不是每次画一个独立圆点否则圆与圆之间会留下未刮到的缝隙而且大量 fill 调用会对性能不友好。这里的 lineWidth24 是笔刷宽度建议根据画布尺寸动态设置。2.3 为什么不引第三方库类型选型与兼容矩阵我在封装前也考虑过直接找现成库但最终决定自己写。原因有几个市面上刮刮卡轮子大多只针对移动端或只针对桌面端两端都维护好的很少第三方绘制库会把 Canvas 封装成自己的渲染模型遇到业务要自定义涂层、兑奖码露出动画时又要再套一层另外刮刮卡的核心逻辑只有 100 多行引一个几十 KB 的库反而让依赖变重。组件内使用原生 Canvas 2D 是兼容性最好的选择从 Vue 2 到 Vue 3、从 PC 到移动端都能跑。如果项目必须支持老版本浏览器需要确认 PointerEvent 是否可用不可用的情况可以用 mouse touch 做降级。下面这张表是选型时我习惯对照的方案优点缺点结论CSS mask实现简单无法按轨迹擦除不选双层 div 矩形不需要 Canvas边缘生硬、颗粒感强不选Canvas 2D像素级控制、兼容性全面需要自己处理事件和缩放选它WebGL性能高复杂度高、上下文管理繁琐不选2.4 刮开率判定算透明像素还是算轨迹面积组件最终要告诉业务方“刮开了多少”常见误区是统计刮擦轨迹的总长度或圆点面积。但轨迹会有大量重叠重复计算会让百分比虚高而且用户刮得细碎时整体覆盖率并不等于轨迹面积。我一般用离屏 Canvas 来算维护一个与主画布尺寸相同的离屏画布每次刮擦时同步在离屏画布上画同样的轨迹然后定时调用 getImageData 统计 alpha 通道接近 0 的像素比例。function getScratchPercent(offCanvas) { const offCtx offCanvas.getContext(2d); const imageData offCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); const pixels imageData.data; let clearCount 0; const total pixels.length / 4; for (let i 3; i pixels.length; i 4) { if (pixels[i] 30) clearCount; } return Math.round((clearCount / total) * 100); }这段代码遍历每个像素的 alpha 通道把 alpha 小于 30 的像素视为“已刮开”。统计时要注意阈值的选取阈值设成 0涂层边缘的抗锯齿像素容易被漏掉设成 3050 比较稳。getImageData 返回的是整个画布的快照反复大量调用会有性能开销所以我会加上节流比如每 100ms 最多统计一次而不是在每次 move 事件里都执行。这一章把原理和选型立住了下一章可以直接进入组件实现。3. 封装组件的第一版Props、事件与 30 行可用代码原理验证通过后剩下就是把刮开逻辑封装成 Vue 组件。我用 Vue 3 的script setup写了一份最小可用版本放在业务里能直接跑如果你是 Vue 2 项目把响应式部分换成 data 和 methodsCanvas 逻辑完全不用改。3.1 最小可用版本先跑通再谈优雅先把能跑的组件贴出来。这个版本不依赖任何第三方库核心逻辑集中在 onMounted、事件回调和暴露方法上。template div classscratch-card :style{ width: width px, height: height px } div classscratch-card__prize slot / /div canvas refcanvasRef classscratch-card__canvas/canvas /div /template script setup import { ref, reactive, onMounted, onBeforeUnmount } from vue; const props defineProps({ width: { type: Number, default: 300 }, height: { type: Number, default: 160 }, brushSize: { type: Number, default: 24 }, coverColor: { type: String, default: #c0c0c0 }, percent: { type: Number, default: 60 } }); const emit defineEmits([progress, complete]); const canvasRef ref(null); const state reactive({ isScratching: false, isDone: false, lastX: 0, lastY: 0 }); let ctx null; let offCanvas null; let offCtx null; let timer null; onMounted(() { const canvas canvasRef.value; canvas.width props.width; canvas.height props.height; ctx canvas.getContext(2d, { alpha: true }); offCanvas document.createElement(canvas); offCanvas.width props.width; offCanvas.height props.height; offCtx offCanvas.getContext(2d, { alpha: true }); drawCover(); canvas.addEventListener(pointerdown, onPointerDown); canvas.addEventListener(pointermove, onPointerMove); canvas.addEventListener(pointerup, onPointerUp); canvas.addEventListener(pointercancel, onPointerUp); }); function drawCover() { ctx.fillStyle props.coverColor; ctx.fillRect(0, 0, props.width, props.height); offCtx.fillStyle props.coverColor; offCtx.fillRect(0, 0, props.width, props.height); } function getPos(e) { const rect canvasRef.value.getBoundingClientRect(); return { x: e.clientX - rect.left, y: e.clientY - rect.top }; } function onPointerDown(e) { if (state.isDone) return; state.isScratching true; const pos getPos(e); state.lastX pos.x; state.lastY pos.y; canvasRef.value.setPointerCapture(e.pointerId); } function onPointerMove(e) { if (!state.isScratching || state.isDone) return; const pos getPos(e); drawLine(state.lastX, state.lastY, pos.x, pos.y); state.lastX pos.x; state.lastY pos.y; } function onPointerUp() { if (!state.isScratching) return; state.isScratching false; } function drawLine(x1, y1, x2, y2) { ctx.save(); ctx.globalCompositeOperation destination-out; ctx.lineCap round; ctx.lineJoin round; ctx.lineWidth props.brushSize; ctx.beginPath(); ctx.moveTo(x1, y1); ctx.lineTo(x2, y2); ctx.stroke(); ctx.restore(); offCtx.save(); offCtx.globalCompositeOperation destination-out; offCtx.lineCap round; offCtx.lineJoin round; offCtx.lineWidth props.brushSize; offCtx.beginPath(); offCtx.moveTo(x1, y1); offCtx.lineTo(x2, y2); offCtx.stroke(); offCtx.restore(); checkProgress(); } function checkProgress() { if (timer) return; timer setTimeout(() { timer null; const percent getScratchPercent(offCanvas); emit(progress, percent); if (percent props.percent !state.isDone) { state.isDone true; emit(complete); } }, 100); } function getScratchPercent(offCanvas) { const pCtx offCanvas.getContext(2d); const imageData pCtx.getImageData(0, 0, offCanvas.width, offCanvas.height); const data imageData.data; let clearCount 0; const total data.length / 4; for (let i 3; i data.length; i 4) { if (data[i] 30) clearCount; } return Math.round((clearCount / total) * 100); } onBeforeUnmount(() { if (canvasRef.value) { canvasRef.value.removeEventListener(pointerdown, onPointerDown); canvasRef.value.removeEventListener(pointermove, onPointerMove); canvasRef.value.removeEventListener(pointerup, onPointerUp); canvasRef.value.removeEventListener(pointercancel, onPointerUp); } if (timer) clearTimeout(timer); }); defineExpose({ reset() { state.isDone false; state.isScratching false; ctx.clearRect(0, 0, props.width, props.height); offCtx.clearRect(0, 0, props.width, props.height); drawCover(); } }); /script style scoped .scratch-card { position: relative; overflow: hidden; -webkit-user-select: none; user-select: none; touch-action: none; } .scratch-card__prize { position: absolute; inset: 0; display: flex; align-items: center; justify-content: center; } .scratch-card__canvas { position: absolute; inset: 0; width: 100%; height: 100%; } /style这里有一个关键点同步维护了一个离屏 canvas保证主 canvas 只负责展示离屏 canvas 只负责计算刮开率。pointerdown/move/up用 PointerEvent 统一处理避免同时写 mouse 和 touch 两套事件样式里的touch-action: none是为了禁止浏览器在刮擦时把 canvas 当成滚动区域。setPointerCapture让手指划过 canvas 边缘后事件依然会回到当前画布不会中途丢失。onPointerDown把当前坐标记为上一笔的起点onPointerMove每次画一条从上一笔到当前位置的线段并在离屏画布同步画一遍。checkProgress用 100ms 节流防止 getImageData 频繁执行。这里先提供最基础版本坐标换算的细节会在下一章展开。3.2 Props 参数设计涂层、笔刷与刮开阈值组件的可复用性主要靠 Props 区分。我习惯给业务侧暴露 width、height、brushSize、coverColor、percent 这几个参数。percent 就是刮开率达到多少后判定完成默认 60brushSize 在 PC 上默认 24在触摸屏上建议按画布宽度比例调大例如width * 0.08。下表是参数的具体建议props类型默认值说明widthNumber300画布 CSS 宽度单位 pxheightNumber160画布 CSS 高度单位 pxbrushSizeNumber24笔刷大小决定刮痕粗细coverColorString#c0c0c0涂层颜色也可以是图片percentNumber60刮开多少百分比判定完成有人会把 width 和 height 直接设为 props.width 赋值给 canvas 的 width 属性这在没有缩放的页面上没问题。但一旦页面有 CSS 缩放、DPR 适配就要把物理像素和逻辑像素分开处理。第 4 章会专门说这个坑。3.3 事件设计progress 和 complete 各在什么时机触发组件对外只暴露两个业务事件progress 返回当前刮开百分比complete 表示达到阈值后的完成通知。注意 complete 必须只在“未完成转完成”的状态边界触发一次否则事件会重复触发。上面的代码用state.isDone做了拦截。业务侧使用方式如下ScratchCard :width320 :height180 :brush-size28 :percent65 progressonProgress completeonComplete div classprize-codeAXJD2025/div /ScratchCard在 onComplete 里你可以去做发奖、埋点、弹窗。只要提供 reset 接口就能继续下一轮。这里要提醒的是progress 事件的频率受 getImageData 节流影响不要期望每次 move 都拿到实时值业务埋点按 100ms 一个点收到就能接受。写完事件组件已经可以在业务里跑但事件层和坐标换算还需要再仔细处理。4. PC 与移动端事件统一PointerEvent 状态机和坐标换算刮刮卡组件能不能在 PC 和移动端同时使用关键不在 Canvas 绘制而在事件层。我见过不少把 mouse 和 touch 分开监听的项目PC 上用 mouseDown/mouseMove/mouseUp手机上用 touchStart/touchMove/touchEnd结果代码分支又多又容易漏掉 touchend 的清理。用 PointerEvent 可以一次性覆盖鼠标、触摸和触控笔。4.1 mouse touch 分写的三个痛点PointerEvent 如何解决第一mouse 事件在移动端并不会全部生效某些浏览器会模拟 click但 move 的连续性和坐标精度都不可靠。第二touch 事件里拿坐标要用e.touches[0]同一个事件对象的结构和 mouse 不同代码里到处是条件分支。第三触摸离开 canvas 之后touch 事件流不一定能正确结束容易出现“一直认为在刮”的状态残留。PointerEvent 把这几种输入统一成 pointerdown、pointermove、pointerup三种输入源都有 clientX/clientY 和 pointerId。组件里只需要关注 pointerId 对应的那个活动指针多个手指时也不会互相干扰。对于老项目我会先判断window.PointerEvent是否存在不存在再退回到 mouse touch 的兼容层但这个组件默认按 PointerEvent 写。4.2 用 PointerEvent 重写刮擦回调状态机与边界处理一组稳定的指针生命周期是pointerdown 进入“正在刮”的状态pointermove 在这个状态下持续绘制pointerup 退出状态pointercancel 也要退出否则来电、系统手势等场景会让状态卡死。在 Vue 组件里维护一个 state 对象isScratching 就是这个状态机的核心标志。function onPointerDown(e) { if (state.isDone) return; state.isScratching true; const pos canvasToPaint(e); state.lastX pos.x; state.lastY pos.y; canvasRef.value.setPointerCapture(e.pointerId); } function onPointerMove(e) { if (!state.isScratching || state.isDone) return; const pos canvasToPaint(e); drawLine(state.lastX, state.lastY, pos.x, pos.y); state.lastX pos.x; state.lastY pos.y; } function onPointerUp(e) { state.isScratching false; }这里有两个容易被忽略的点。一是setPointerCapture必须在 pointerdown 里调用否则手指移动到 canvas 外部时后续 pointermove 会不再派发到 canvas二是 pointermove 的触发频率远高于屏幕刷新率如果不加节流Canvas 的 stroke 会积压。我一般在刮擦回调里直接画线因为 Canvas 的 stroke 本身是增量绘制不需要清空画布高频执行的压力远低于 getImageData。4.3 canvas 坐标换算与 devicePixelRatio 的尺寸陷阱在 PC 上我们直接用clientX - rect.left得到画布坐标但在移动端和高分屏上这个值是错的。浏览器中 canvas 元素有两个尺寸一个是 CSS 尺寸由样式决定一个是画布坐标尺寸由 width/height 属性决定。当我们为了清晰度把 canvas.width 设为 CSS 宽度的 2 倍时用clientX - rect.left拿到的只是逻辑坐标必须乘以缩放系数才能对应到物理像素。处理方式是统一封装一个canvasToPaint(e)函数function canvasToPaint(e) { const canvas canvasRef.value; const rect canvas.getBoundingClientRect(); const scaleX canvas.width / rect.width; const scaleY canvas.height / rect.height; return { x: (e.clientX - rect.left) * scaleX, y: (e.clientY - rect.top) * scaleY }; }如果 canvas.width 和 CSS width 一致scaleX 就是 1如果按 devicePixelRatio 放大scaleX 也会自动放大。注意在绘制涂层时也要按放大后的画布尺寸绘制否则涂层只覆盖了左上角一部分。建议组件初始化时这样处理const dpr window.devicePixelRatio || 1; canvas.width props.width * dpr; canvas.height props.height * dpr; canvasRef.value.style.width props.width px; canvasRef.value.style.height props.height px; ctx.scale(dpr, dpr);调用 canvasToPaint 时canvas.width 已经是物理宽rect.width 是逻辑宽scaleX 正好是 dpr拿到的坐标也是物理坐标而此时 ctx 已经 scale 了 dpr所以可以直接在 drawLine 里用这个坐标绘制。这套逻辑比“每个事件都除以 dpr”更省心也避免后期在多个入口改参数。到这里事件层的核心问题都已经处理完接下来集中写我踩过的坑。5. 刮刮卡组件避坑指南从刮不开到卡成 PPT 的五个案例这一章的内容大多来自真实调试记录。每个问题都按照“现象 → 原因 → 解决”的顺序整理方便直接对照。5.1 现象手机上刮完没有任何效果涂层纹丝不动在 iOS 上 canvas 绑定了 pointermove但刮的时候页面会在原地滚动涂层没有任何变化。原因通常是浏览器的触摸动作默认由系统处理canvas 上的 touchmove 被当成了滚动PointerEvent 的默认行为没有及时阻止而且 canvas 样式没有设置touch-action: none。解决方法是给刮刮卡容器显式加touch-action: none同时组件初始化时确认没有在全局样式中覆盖掉它。另外监听 touch 事件的老写法一定记得给addEventListener(touchmove, handler, { passive: false })否则 preventDefault 根本无效这一点最容易踩。5.2 现象刮开后涂层变成黑色或灰色底层内容显示不完整如果刮开区域露出来的是深色而不是底层兑奖码先检查 Canvas 上下文是否带了 alpha。getContext(2d, { alpha: false })会把画布背景当成不透明黑色destination-out 擦掉涂层之后露出的黑色其实是画布底色。解决方法是显式使用{ alpha: true }并且不要在 CSS 里给 canvas 设置背景色。另一种情况是底层内容z-index比 canvas 高canvas 虽然透明但依然盖住了底层检查.scratch-card__prize的位置是否在 canvas 下方。样式上我固定让 canvas 绝对定位、prize 容器放在它前面这样 DOM 层级靠后的 canvas 遮住 prize透明区域就能露出底层而不是反过来。5.3 现象刮到百分之七十了complete 事件迟迟不触发complete 不触发通常是百分比判定口径不对。有人统计的是刮擦轨迹圆形面积之和但轨迹重叠导致面积虚高后面刮新区域时累计涨幅却很小有人用 getImageData 但没有充分考虑涂层边缘的抗锯齿像素alpha 不为 0 但接近 0阈值设成 0 后这些像素从来没算作刮开。解决方法是把阈值设成 30并且在刮开判定前先做一次离屏 canvas 的整体采样。另一个坑是节流和事件顺序如果用 setTimeout 做节流但 setTimeout 执行时刮擦已经停止百分比更新会比实际晚一拍需要再 onPointerUp 时立刻计算一次或者把节流函数设计成时间戳。5.4 现象PC 上正常手机上一刮就位置偏移且画面模糊高分屏的 devicePixelRatio 通常大于 1如果 canvas 的 width/height 只按 CSS 尺寸设置物理像素不足画面一放大就糊如果只放大物理尺寸而没有同步做坐标换算刮擦位置就会对不上。解决方式就是第 4 章的canvas.width cssWidth * dpr和 canvasToPaint 缩放系数。注意 dpr 不是固定的不同设备可能是 2、2.75 或 3不能写死某个值。组件里我习惯在 onMounted 读取window.devicePixelRatio || 1并且在窗口 resize 后重新初始化画布。5.5 现象页面上放了三张刮刮卡刮第一张时第二张也被刮开事件监听和组件实例管理问题。如果事件监听被挂到 window 上且在回调中使用“获取页面上特定 class 的 canvas”多个实例就会互相干扰还有一种情况是组件没有在 onBeforeUnmount 移除监听组件被 keep-alive 缓存后旧的监听还在点上新的卡会同时触发旧实例的回调。解决原则是事件监听只挂在当前组件自己的 canvas 元素上所有状态只存在当前实例的闭包中离开页面时统一 removeEventListener。给出的最小版本里已经写了移除逻辑如果业务里需要批量渲染最好给每个实例加唯一标识在事件对象中通过 dataset 标记。6. 进阶加分项离屏 Canvas 算刮开率与组件接口扩展组件能用了但还有三个点会直接影响活动页体验进度计算不能卡 UI、业务需要手动重置或自动刮开、多端真机自检。6.1 用离屏 Canvas 采样进度计算不占用渲染线程主 canvas 每帧都在被浏览器绘制如果每次刮擦都在主 canvas 上调 getImageData会把整张画布的像素读回到 CPU移动端低端机很容易掉帧。我的习惯是额外创建一个不在 DOM 中的离屏 canvas绘制刮痕时同步画到离屏画布进度统计只读离屏画布的像素。这样主 canvas 和统计逻辑彻底分离即使采样频率再高也不会让画面卡顿。function checkProgress() { const percent getScratchPercent(offCanvas); emit(progress, percent); if (percent props.percent !state.isDone) { state.isDone true; ctx.clearRect(0, 0, props.width, props.height); emit(complete); } }注意这里达到阈值后清空主 canvas避免涂层残留和反复刮擦。有些业务希望刮开后保留刮痕这个行为可以做成 prop 控制。6.2 给组件增加 reset 和 scratchAll 两个对外方法业务场景经常是用户刮完一次后刷新奖品或者运营想直接展示结果。通过 defineExpose 暴露 reset 和 scratchAll 是低成本方案。reset 清空涂层并重置状态scratchAll 则把涂层直接清掉并触发完成事件。defineExpose({ reset() { state.isDone false; state.isScratching false; ctx.clearRect(0, 0, props.width, props.height); offCtx.clearRect(0, 0, props.width, props.height); drawCover(); }, scratchAll() { state.isDone true; state.isScratching false; ctx.clearRect(0, 0, props.width, props.height); offCtx.clearRect(0, 0, props.width, props.height); emit(complete); } });调用方式是在父组件拿到模板 ref然后scratchCardRef.value.reset()。注意 reset 后要主动处理上一轮的 complete 事件避免旧事件对新的状态造成干扰。6.3 上线前我习惯做三个真机验证组件最终要在 PC 和移动端同时使用我的自检顺序是先用桌面浏览器鼠标拖拽确认刮痕圆滑连续再用桌面浏览器的开发者工具触摸模拟确认 pointer 事件能正常触发最后拿一台 iOS 真机和一台中端安卓真机重点看刮开后是否透明、是否出现滚动以及安卓真机上连续刮 10 秒会不会掉帧。这类交互问题在真机上远比在模拟器上明显。我自己在这个组件上翻车最多的一次是只测了浏览器触摸模拟就上线结果 iOS 真机上涂层刮不掉。原因是桌面触摸模拟不会完全复现触摸滚动行为必须真机确认touch-action: none真正生效。另一个习惯是给组件预留debug模式显示当前刮开百分比和 pointerId排查时比肉眼猜快很多。把这三个加分项补齐之后这个刮刮卡组件就不只是“能刮出来”而是可以放心投到活动页并且能自己掌控细节的完整业务组件了。希望这些从原理到踩坑的笔记能帮到你。本文还有配套的精品资源点击获取

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑