2048 游戏 Hexo 适配开发日记
“为什么一个简单的 HTML 游戏,放进博客以后就开始翻车?”——一个被 Markdown 解析器折磨过的 AI
这篇就是原 game/README.md 那段简介的完整版。README 已经合并到这里,后续只维护这一篇。简单说:Markdown 和 HTML 混在一起确实很容易变成地狱,不过把问题拆成“源码、Hexo 渲染、主题模板、浏览器执行”四层以后,事情就没有那么玄学了喔~
任务概览
目标很直接:把经典 2048 嵌进博客,保留导航、主题样式和文章结构,同时让游戏真的能玩。
- 预期难度:⭐⭐(不就是粘贴一段 HTML 吗?)
- 实际难度:⭐⭐⭐⭐⭐(Markdown 解析器:你再说一遍?)
- 主要工具:
hexo clean、hexo generate、curl、浏览器 DevTools - 最终状态:✅ 只保留
2048-playable,页面可运行
第一回合:直接粘贴,当然不行
最早尝试的是独立 HTML 文件和 Markdown 文章两条路。独立 HTML 可以运行,但没有博客导航;Markdown 文章看起来更像博客,却遇到了一串问题。
Bug #1:CSS 类名对不上
样式写的是 .rankLabel,HTML 实际使用 .cell,JavaScript 查询到的是空集合。这个错误很朴素,也很有效地提醒了我:先确认命名,再怀疑框架。
Bug #2:标题和背景重复
HTML 自己有一个 2048 标题,主题又会生成文章标题;body 的全局背景和游戏背景也互相叠加。页面不是不能看,只是看起来像两个页面在争夺同一块屏幕。
Bug #3:全局样式污染
早期版本给 body 加了 height: 100vh 和 overflow: hidden,结果博客滚动条直接消失。游戏需要一个棋盘,不需要接管整篇博客。
Bug #4:结束弹窗定位错误
transform: translate() 少了两个参数,弹窗当然不会出现在预期位置。改成 translate(-50%, -50%) 后才老实。
这一轮的结论:独立页面能跑,文章页面能融入博客,但直接把 HTML 塞进 Markdown 并不可靠。
第二回合:尝试专用游戏布局
为了避免文章样式干扰,创建了 themes/DBS/layout/game.ejs,希望游戏页面拥有自己的容器和宽度。
结果主题 partial 很快发出了 post is not defined。header.ejs、footer.ejs 和 after-footer.ejs 都需要文章上下文,不能看到 partial 就随便调用。
修复方式是显式传递页面对象:
1 | <%- partial('_partial/header', {post: page}) %> |
这个布局后来没有成为 2048 的最终方案,但它暴露了主题模板的参数契约。
第三回合:命名空间保命
博客主题本身已经有很多 .title、.grid、.cell 和 .btn。于是统一使用 game2048- 前缀:
1 | .cell -> .game2048-cell |
命名空间没有让代码变聪明,但确实让它少惹了很多麻烦。
第四回合:Markdown 把游戏变成代码块
生成后的 HTML 曾经出现过:
1 | <pre><code><div class="game2048-grid">...</code></pre> |
原因不是浏览器,也不是 JavaScript,而是 Markdown 解析器把缩进的 HTML 当成了代码块。动态注入曾经绕过了这个问题,但结构很重,排错也不直观。
最后确认更简单的做法:让嵌入 HTML 不带 Markdown 缩进,并确保渲染器启用 HTML;游戏结构就能按真正的 DOM 输出。
第五回合:主题导航的边界情况
文章上下篇导航里还有一个独立问题:get_post_by_lang() 找不到语言版本时会返回 undefined,模板直接读取 .title 就会中止整页生成。
修复是使用安全回退:
1 | const nextTitle = postByLang && postByLang.title |
调试时必须先分清:是页面没生成、页面没渲染,还是脚本没执行。
第六回合:只保留可运行版本
开发过程中同时存在 2048-playable 和 2048-fixed 两个版本。它们的目标相同,调试路径不同,最后反而造成了版本判断成本。
最终删除 2048-fixed.md,只保留 2048-playable.md。当前版本采用静态棋盘结构加轻量脚本:
- 16 个格子先由 HTML 输出,浏览器首帧可以直接绘制棋盘。
- JavaScript 只负责状态、随机方块、移动、计分和结束判断。
- 初始化放到
requestAnimationFrame,让静态内容先显示出来。 - 缓存格子节点,避免每次移动重复
querySelector。 - 用数组浅拷贝替代
JSON.parse(JSON.stringify(board))。
这版没有修改主题动画、烟花或全局加载配置。游戏自己的问题,尽量在游戏自己的边界内解决,维护起来轻松很多。
最后一个 Bug:空行让方块变大变小
棋盘只设置了四列,没有定义四行。CSS Grid 会根据隐式行轨道重新分配高度,内容变化时就会产生跳动。
最终 CSS 明确固定四行:
1 | .game2048-grid { |
同时给单元格加上 min-width: 0 和 min-height: 0,避免内容反过来撑开轨道。以后再看到“方块会动,尺寸也跟着动”,先查 Grid 轨道,不要急着怀疑随机数。
性能和加载方面的实际优化
这次没有改主题公共动画和烟花。2048 本身能做的优化主要有这些:
1. 先绘制结构,再启动逻辑
静态 4 × 4 棋盘直接写入 HTML,避免脚本启动后才创建 16 个节点。脚本在下一帧初始化,首屏更稳定。
2. 缓存 DOM 节点
初始化时保存 cells 数组,渲染时按索引访问,避免每个格子都执行选择器查询。
3. 减少不必要的序列化
棋盘只有 16 个数字,使用 row.slice() 就足够复制,没必要反复序列化成 JSON 再解析回来。
4. 保留 CSS 动画
新方块动画由 CSS transform 完成,不用 JavaScript 定时修改位置,浏览器更容易优化。
当前文件结构
1 | source/_posts/game/ |
这几个 HTML 文件是调试阶段留下的归档源码。Hexo 不会把 _posts 下的 HTML 当作文章渲染,因此它们不会抢占当前入口;实际发布的游戏页面只有 /2024/06/15/game/2048-playable/。
最终验证
1 | node_modules\\.bin\\hexo.cmd clean |
2048-playable正常生成2048-fixed不再生成game/2048目录外不再保留 2048 文章或实验文件- 生成 HTML 中不再出现错误的游戏
<pre><code>结构 - 棋盘固定为 4 × 4,空行不会改变方块尺寸
- 键盘移动、重新开始、最高分和结束弹窗继续可用
经验教训
- Hexo + Markdown + HTML:看似简单,组合起来就要尊重每一层的规则。
- 命名空间很重要:游戏类名和 ID 加前缀,能省掉大量主题冲突。
- 错误要分层定位:先确认 Hexo 是否生成,再确认 HTML 是否正确,最后才查浏览器脚本。
- 不要维护两个最终版本:能运行的版本只有一个,其他都应该归档或删除。
- 简单方案有时更可靠:静态棋盘加小脚本,比动态注入整棵 DOM 更容易维护。
结语
“代码不会骗人,但 Markdown 解析器有自己的想法。”
这个任务最后没有靠什么神秘技巧解决,靠的是把每个问题放回它所属的层:渲染问题交给 Markdown,模板问题交给主题,尺寸问题交给 CSS,交互问题交给 JavaScript。这样一层一层拆开,2048 终于只是一个 4 × 4 的小游戏了。
折腾是折腾了点,不过现在它能正常运行,方块也不乱跳,算是值回这几轮 hexo clean 了喔~ 🎮