简介这份资源是一套面向网页开发初学者的HTML贪吃蛇游戏实战项目适合刚接触前端、希望把HTML、CSS与JavaScript串联起来练手的学习者。项目围绕游戏区域搭建、分数显示、键盘事件监听、蛇身位置更新、碰撞检测与食物随机生成等核心逻辑展开能帮助读者理解静态页面结构与脚本逻辑如何协同工作并在此基础上尝试CSS3动画、音效或难度分级等扩展。压缩包共3个文件包含1个html主页面、1张jpg背景图与1张png素材图整体约86KB体积轻巧便于直接打开运行和阅读源码。目前已有1452人学习下载说明该案例在入门练习中具有一定参考价值。通过这份资源读者可以获得一份可运行的完整小游戏代码对照理解DOM操作、定时器与条件判断的实际用法并借助现成素材快速还原游戏界面适合作为前端基础阶段的综合练习素材。1. 用 HTML 做贪吃蛇一个文件就能跑但 90% 的人卡在坐标系上很多人第一次听到「用 HTML 做贪吃蛇」脑子里浮现的是打开记事本、敲几行标签、双击浏览器就能玩。真动手才发现蛇不动、吃到食物不增长、撞墙判定失灵甚至键盘按下去页面先滚动了。问题不在 HTML 本身而在于这个项目本质是「HTML 搭骨架 CSS 画格子 JavaScript 跑逻辑」的三件套HTML 只负责那块画布和页面结构真正让蛇活起来的是后面两层。这份资源就是围绕这个最小闭环展开的一个.html文件里同时容纳结构、样式和脚本不依赖任何构建工具、不装 npm、不引外部库双击即运行。适合刚学完!doctype html、meta charsetutf-8这些基础标签、想找个能立刻看到反馈的小项目练手的人也适合想拿它当教学演示、给学员讲清楚「DOM 操作 定时器 二维网格」三者怎么配合的从业者。它解决的不是「做一个商业级游戏」而是「用最短路径把抽象语法变成看得见的交互」。2. 拆开这个单文件HTML 骨架、CSS 网格与 JS 循环各管什么2.1 为什么选 Canvas 而不是一堆 div用 HTML 做贪吃蛇绕不开的第一个选型就是「蛇身用什么画」。常见做法有两种一种是用几十个div拼成网格靠改style.background来显示蛇和食物另一种是用一个canvas元素所有绘制都在 JS 里通过fillRect完成。前者对新手友好因为每个格子都是真实 DOM改颜色直观但蛇一长DOM 节点数量上去重绘和布局计算会拖慢帧率而且坐标和 DOM 位置容易对不齐。后者把渲染收进一块画布逻辑坐标和像素坐标一一对应性能稳定代价是要理解getContext(2d)这套 API。我一般会推荐 Canvas 方案原因很实际贪吃蛇的移动是「整格跳变」不是平滑动画用 Canvas 每帧清空重画反而比操作几十个 div 更简单、更可控。下面这个骨架就是最小可运行版本的结构注意meta charsetutf-8和viewport这两行少了它们中文会乱码、手机上会缩放异常。!doctype html html langzh-cn head meta charsetutf-8 meta nameviewport contentwidthdevice-width, initial-scale1 title贪吃蛇/title style /* 画布居中去掉页面默认边距避免方向键触发滚动 */ body { margin: 0; display: flex; justify-content: center; align-items: center; height: 100vh; background: #1e1e1e; overflow: hidden; } canvas { background: #111; border: 2px solid #333; } /style /head body canvas idgame width400 height400/canvas script // 逻辑全部写在这里下一节展开 /script /body /html这段结构里width和height直接写在canvas标签上而不是用 CSS 控制是因为 Canvas 的绘制坐标系由这两个属性决定CSS 只负责显示尺寸。如果两者不一致画面会被拉伸模糊。overflow: hidden是为了防止方向键按下时页面整体滚动这是新手最常遇到的「玄学」之一。2.2 网格坐标与游戏循环把「移动」翻译成数学贪吃蛇的核心不是画图而是「状态更新」。把画布切成 20×20 的格子每格 20 像素蛇头坐标用{x, y}表示格子索引而不是像素。每次移动就是根据当前方向把蛇头坐标加一或减一再把新头插入蛇身数组头部然后判断是否吃到食物吃到就不删尾部没吃到就删掉尾部。这个「头插尾删」的队列操作就是蛇移动的全部秘密。游戏循环用setInterval或requestAnimationFrame驱动。setInterval简单固定间隔执行一次更新requestAnimationFrame跟屏幕刷新率同步更平滑但需要自己控制时间差。对贪吃蛇这种整格跳变的游戏setInterval足够间隔设 120150 毫秒手感比较稳。下面这段是可直接抄的核心逻辑注意方向判断里那个「不能 180 度掉头」的检查少了它蛇会瞬间咬到自己。const canvas document.getElementById(game); const ctx canvas.getContext(2d); const CELL 20; // 每格像素 const COLS canvas.width / CELL; const ROWS canvas.height / CELL; let snake [{x: 10, y: 10}]; // 蛇身数组索引 0 是头 let dir {x: 1, y: 0}; // 当前方向初始向右 let nextDir {x: 1, y: 0}; // 缓冲方向避免同一帧内多次转向 let food {x: 15, y: 10}; let timer null; function spawnFood() { // 随机生成食物且不能落在蛇身上 let p; do { p { x: Math.floor(Math.random() * COLS), y: Math.floor(Math.random() * ROWS) }; } while (snake.some(s s.x p.x s.y p.y)); food p; } function update() { dir nextDir; // 每帧只采纳一次转向 const head { x: snake[0].x dir.x, y: snake[0].y dir.y }; // 撞墙判定超出网格即结束 if (head.x 0 || head.x COLS || head.y 0 || head.y ROWS) { return gameOver(); } // 撞自己判定新头坐标与身体重合 if (snake.some(s s.x head.x s.y head.y)) { return gameOver(); } snake.unshift(head); // 头插 if (head.x food.x head.y food.y) { spawnFood(); // 吃到食物不删尾蛇变长 } else { snake.pop(); // 没吃到删尾长度不变 } draw(); } function draw() { ctx.clearRect(0, 0, canvas.width, canvas.height); ctx.fillStyle #4caf50; snake.forEach(s ctx.fillRect(s.x * CELL, s.y * CELL, CELL - 2, CELL - 2)); ctx.fillStyle #f44336; ctx.fillRect(food.x * CELL, food.y * CELL, CELL - 2, CELL - 2); } function gameOver() { clearInterval(timer); ctx.fillStyle #fff; ctx.font 24px sans-serif; ctx.fillText(Game Over, 120, 200); } document.addEventListener(keydown, e { const map { ArrowUp: {x: 0, y: -1}, ArrowDown: {x: 0, y: 1}, ArrowLeft: {x: -1, y: 0}, ArrowRight: {x: 1, y: 0} }; const d map[e.key]; if (!d) return; e.preventDefault(); // 阻止方向键滚动页面 // 禁止 180 度掉头新方向不能与当前方向相反 if (d.x -dir.x d.y -dir.y) return; nextDir d; }); spawnFood(); draw(); timer setInterval(update, 130);参数上CELL决定格子大小改小格子更多、蛇更细腻但视觉上蛇变细setInterval的 130 毫秒是速度数值越小蛇跑得越快低于 80 毫秒多数人反应不过来。nextDir这个缓冲变量是关键如果直接改dir玩家在两次更新之间快速按两个方向键可能在同一帧内先转再转导致蛇直接反向撞死自己。用nextDir暂存、每帧只采纳一次就绕开了这个坑。2.3 键盘事件与移动端适配的取舍桌面端靠keydown监听方向键逻辑简单。但这份资源如果只在电脑上跑价值就少了一半。移动端没有物理方向键常见做法是加四个屏幕按钮或者监听touchstart/touchmove判断滑动方向。加按钮最稳不依赖手势识别代码量也小滑动手势体验好但阈值设不好会误判。我一般会先加按钮保证能玩再补滑动作为增强。// 移动端四个方向按钮绑定与键盘相同的转向逻辑 [up,down,left,right].forEach(id { document.getElementById(id).addEventListener(touchstart, e { e.preventDefault(); const d { up:{x:0,y:-1}, down:{x:0,y:1}, left:{x:-1,y:0}, right:{x:1,y:0} }[id]; if (d.x -dir.x d.y -dir.y) return; nextDir d; }); });注意touchstart里也要做反向检查否则手机上同样会咬到自己。按钮的 HTML 结构需要额外加在canvas旁边用 CSS 固定在底部这里不展开思路是「按钮只负责改nextDir不直接操作蛇」。3. 从能跑到好玩分数、加速与暂停的实现细节3.1 分数显示与难度递增能跑起来之后第二个需求通常是「记分」和「越吃越快」。分数就是蛇身长度减初始长度或者每吃一个加 10 分直接画在画布上或用一个span显示。难度递增的做法是每吃到一个食物把setInterval的间隔减少几毫秒但要用clearInterval先清掉旧的再设新的否则会同时跑多个定时器蛇会突然加速到失控。let score 0; let speed 130; function restartTimer() { clearInterval(timer); timer setInterval(update, speed); } // 在吃到食物的分支里 score 10; speed Math.max(60, speed - 4); // 下限 60 毫秒防止快到无法操作 restartTimer();speed的下限一定要设我见过有人不设下限玩到后面间隔变成 10 毫秒蛇每帧移动一格肉眼根本看不清直接变成「必死局」。60 毫秒是多数人还能反应的极限再快就失去游戏意义了。3.2 暂停与重开的边界处理暂停不是简单地把定时器停掉就完事。停掉之后dir和nextDir还保留着恢复时如果玩家在暂停期间按了方向键nextDir已经变了恢复瞬间蛇会朝新方向走可能直接撞墙。稳妥做法是暂停时同时冻结输入或者恢复时把nextDir重置为dir。重开则要把蛇、分数、速度、食物全部初始化回初始状态少重置任何一个下一局都会带着上一局的残留。let paused false; function togglePause() { if (paused) { paused false; nextDir dir; // 恢复时对齐方向避免暂停期间的输入生效 restartTimer(); } else { paused true; clearInterval(timer); } }重开函数里snake [{x:10,y:10}]、dir {x:1,y:0}、nextDir dir、score 0、speed 130、spawnFood()、restartTimer()这几步一个都不能少。尤其是nextDir很多人只重置dir结果重开后第一次按键就反向。3.3 用 localStorage 存最高分最高分是让这个小项目「有粘性」的最低成本改动。用localStorage存一个数字游戏结束时比较并更新页面加载时读出来显示。注意localStorage存的是字符串读出来要parseInt否则比较时会出现100 90这种字符串和数字混比的怪结果。function updateBest() { const best parseInt(localStorage.getItem(snake_best) || 0, 10); if (score best) { localStorage.setItem(snake_best, String(score)); } }这段逻辑放在gameOver里调用即可。localStorage按域名隔离本地双击打开的文件file://在不同浏览器里行为可能不一致Chrome 下通常可用但如果你把它放到某个静态服务器上表现最稳定。4. 避坑与排查蛇不动、穿墙、按键失灵的真实原因4.1 蛇按了键没反应或者一按就反向撞死现象是方向键按下去蛇不动或者刚按一下蛇就掉头咬到自己。原因通常有两个一是keydown里没有e.preventDefault()方向键触发了页面滚动焦点跑掉了二是直接修改了dir而没有用nextDir缓冲导致同一帧内多次转向叠加。解决方式是加上preventDefault并且坚持「输入只改nextDirupdate里才把nextDir赋给dir」这条纪律。另外检查map里的键名是否和e.key完全一致ArrowUp不是Up大小写和拼写错一个字符就静默失效。4.2 蛇从右边出去又从左边冒出来现象是蛇撞到边界没有结束而是从对面穿出来。原因是撞墙判定写成了取模运算比如head.x (head.x COLS) % COLS这是「环绕」逻辑不是「撞墙」逻辑。贪吃蛇默认应该是撞墙即死如果你确实想要环绕玩法那要明确这是另一种规则。排查时直接看update里边界判断那几行确认用的是 0和 COLS这种比较而不是%。4.3 吃到食物后蛇变长了两格现象是吃一个食物蛇身长度增加了两格甚至更多。原因通常是unshift之后又忘了pop的条件分支写反了或者在spawnFood里又对蛇做了操作。正确逻辑是unshift永远执行pop只在「没吃到」时执行。如果写成「吃到时 unshift 两次」或者「没吃到时也 unshift」长度就会异常。排查时在update末尾打印snake.length吃食物前后各看一次差值应该是 1。4.4 游戏越玩越卡蛇身多了以后明显掉帧现象是蛇短的时候流畅蛇一长就卡顿。原因多半是每帧都在创建新的 DOM 或者频繁读写localStorage也可能是draw里用了shadowBlur这类高开销效果。Canvas 方案本身不该卡如果卡了先检查draw里有没有在循环内做字符串拼接或 DOM 查询。把ctx.fillStyle的设置提到循环外snake.forEach里只做fillRect能省下大量重复赋值。另外确认没有在update里调用setInterval嵌套那会导致定时器数量指数增长。4.5 手机上按钮点了没反应或者页面跟着滑动现象是移动端按钮触摸无响应或者一触摸页面就上下滑动。原因是touchstart没有preventDefault浏览器把触摸当成了页面滚动手势。解决方式是在按钮的touchstart里调用e.preventDefault()同时给按钮加touch-action: none的 CSS。如果用的是click事件移动端会有 300 毫秒延迟手感很差所以优先用touchstart。另外注意按钮的z-index不要被画布盖住否则点不到。5. 进阶技巧把单文件拆成可维护结构并用帧同步替代定时器单文件跑通之后下一步通常是「怎么让它不那么像玩具」。我的习惯是先把逻辑按职责拆成三个对象Snake管蛇身和移动Food管生成和碰撞Game管循环、输入和状态。拆完之后update里只调用snake.move()、判断food.eatenBy(snake)可读性立刻不一样。但拆的时候有个边界要注意不要为了拆而拆贪吃蛇总共几百行拆成十个文件反而增加跳转成本三个对象是甜点区。第二个进阶点是把setInterval换成基于时间戳的帧同步。setInterval的问题是它不保证精确间隔浏览器标签页切到后台时会被节流切回来可能一次性补执行多次update蛇瞬间移动好几格直接撞死。用requestAnimationFrame配合累积时间差可以做到「不管帧率多少逻辑更新频率恒定」。let lastTime 0; let acc 0; const STEP 130; // 逻辑更新间隔毫秒 function loop(now) { if (!lastTime) lastTime now; acc now - lastTime; lastTime now; // 累积到 STEP 才更新一次逻辑避免后台切回时爆发 while (acc STEP) { update(); acc - STEP; } requestAnimationFrame(loop); } requestAnimationFrame(loop);这段代码里STEP就是原来的setInterval间隔acc是时间累加器。关键在while而不是if它保证即使某一帧间隔偏长也会按固定步长补上逻辑更新而不是一次跳一大步。同时acc在每轮减去STEP不会无限累积。切到后台再回来时now - lastTime可能很大但while会把它拆成多次标准步长的更新蛇的移动节奏保持一致不会出现「瞬移撞墙」的翻车现场。验证这套帧同步是否生效有个简单办法在update里console.log(performance.now())正常应该看到相邻两次更新的时间差稳定在 130 毫秒左右波动不超过十几毫秒。如果差值是 0 或者几百毫秒跳变说明acc的累加或扣减写错了。另外记得在gameOver里把requestAnimationFrame的循环也停掉否则游戏结束后update还在跑分数会继续变。我一般会用一个running标志位loop开头判断if (!running) return;重开时再置回true。从那以后我每次写这类小游戏都强制先跑一遍「后台切换测试」切到别的标签页等十秒再切回来看蛇有没有异常移动。这个习惯帮我省掉了无数次「明明本地跑得好好的一演示就翻车」的尴尬。希望帮到你。本文还有配套的精品资源点击获取