2048 移动端踩坑记:touch-action、24px 阈值和 localStorage 的异常
手机上玩 2048,最容易坏在三件事上:手指一滑页面先跟着滚、滑半天没反应、刷新后最高分没了。我这份 2048 单文件版上线后,这三个坑我都踩过。这篇说说具体的代码怎么改。
一、页面跟着滚:一行 CSS 加一个 passive:false
最初的问题是:手指在棋盘上往左滑,方块没动,页面先滚下去了。原因是浏览器默认把触摸行为判给滚动,等你的 touchend 触发时,页面已经跑远了。
第一件事是给棋盘加一行 CSS:
.board {
touch-action: none;
}
这一句告诉浏览器:这块区域的触摸别做默认手势,交给我自己处理。光有这句还不够。如果监听器的第三个参数没写,Chrome 会默认判定成 passive,也就是「这个监听器不会阻止滚动」,于是 preventDefault() 直接失效、控制台还会警告。
正确的写法是把 passive 显式关掉:
boardEl.addEventListener("touchmove", function (e) {
e.preventDefault();
}, { passive: false });
两个一起做,才真正把滚动拦下来。
二、手指滑了没反应:24 像素阈值
第二版的问题是太灵敏。手指刚落下、轻微一抖就被当成一次滑动,棋盘莫名其妙地动一下。玩家会以为是自己没按准。
我加了一个最小位移阈值:
var absX = Math.abs(dx);
var absY = Math.abs(dy);
if (Math.max(absX, absY) < 24) { touchStart = null; return; }
24 这个数是在真机上试出来的:太小了手抖会误触,太大了正常滑动会觉得迟钝。二十几像素这个区间相对舒服,手指在屏幕上有个稳定的移动意图才会被认。
然后判断方向:
if (absX > absY) { move(dx > 0 ? "right" : "left"); }
else { move(dy > 0 ? "down" : "up"); }
取横竖位移里绝对值更大的那个当方向,这样斜着滑也能得到玩家想要的那个方向,不会出现「我想往左、它往上了」的情况。
三、只记一个起点就够
很多人写触摸逻辑会记一串轨迹点做手势识别。2048 不需要。滑动的本质是起点到终点的位移,中间轨迹没有意义。
var touchStart = null;
boardEl.addEventListener("touchstart", function (e) {
if (e.touches.length === 1) {
touchStart = { x: e.touches[0].clientX, y: e.touches[0].clientY };
}
});
boardEl.addEventListener("touchend", function (e) {
if (!touchStart) return;
var t = e.changedTouches[0];
var dx = t.clientX - touchStart.x;
var dy = t.clientY - touchStart.y;
// 阈值判断 + 方向判定
touchStart = null;
});
两个容易漏的细节:touchstart 要判断 e.touches.length === 1,多指同时按会算乱;touchend 从 changedTouches 拿坐标,因为 touches 在 touchend 时已经空了。
四、刷新后最高分没了:一定要包 try/catch
最高分存 localStorage,最简单的写法是:
best = Math.max(best, score);
localStorage.setItem("best2048", String(best));
在隐私模式、或者浏览器禁用了存储的情况下,setItem 会直接抛异常。这个异常如果没接,整个脚本从这一行开始中断,后面的渲染全部不执行——表现出来就是页面白屏、方块全没了。
所以必须包:
try {
localStorage.setItem("best2048", String(best));
} catch (e) {}
读的时候同理,读失败就给个默认值:
var best = 0;
try {
best = parseInt(localStorage.getItem("best2048") || "0", 10) || 0;
} catch (e) { best = 0; }
这里 parseInt 后面还要 || 0:如果存进去的字符串被改坏了,parseInt 会返回 NaN,NaN 参与比较会让整个分数逻辑失效。
五、一个我漏过的判定
棋盘填满就弹游戏结束,这是错的。满格但还有相邻相同的数字时,玩家仍然能合。必须遍历检查右邻和下邻:
function canMove(g) {
for (var r = 0; r < SIZE; r++) {
for (var c = 0; c < SIZE; c++) {
if (g[r][c] === 0) return true;
if (c < SIZE - 1 && g[r][c] === g[r][c + 1]) return true;
if (r < SIZE - 1 && g[r][c] === g[r + 1][c]) return true;
}
}
return false;
}
没查相邻格那版,我玩到一半明明还能合,界面先弹出「游戏结束」,很挫败。加了这个判断之后才正常。
六、这几处改完的实际效果
从这几处改完,手机上玩起来就顺了:手指左滑棋盘就左移、页面不动;轻轻碰一下不会误触;关掉浏览器再打开,最高分还在。
代码完整放在 GitHub(https://github.com/zc12265793-art/huawei-2048),单文件 index.html 双击就能跑,也可以用 python -m http.server 起个本地服务在手机上看效果。这几点改动加起来不到四十行,但对移动端体验影响很大。
- 点赞
- 收藏
- 关注作者
评论(0)