2048 移动端踩坑记:touch-action、24px 阈值和 localStorage 的异常

举报
yd_231995061 发表于 2026/11/04 12:31:27 2026/11/04
【摘要】 手机上玩 2048 最容易坏在三件事:滑动时页面跟着滚、轻微触碰误判成滑动、刷新后最高分丢失。这篇讲清这三个坑的具体代码改法。

手机上玩 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 起个本地服务在手机上看效果。这几点改动加起来不到四十行,但对移动端体验影响很大。

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0)

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。