{"id":16498348,"url":"https://github.com/geekskai/react-ts-tetris","last_synced_at":"2026-05-15T05:36:02.651Z","repository":{"id":113251640,"uuid":"339622329","full_name":"geekskai/react-ts-tetris","owner":"geekskai","description":null,"archived":false,"fork":false,"pushed_at":"2021-02-17T05:38:42.000Z","size":248,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":1,"default_branch":"master","last_synced_at":"2025-01-12T07:40:49.782Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"JavaScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":null,"status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/geekskai.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":null,"code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2021-02-17T05:36:52.000Z","updated_at":"2021-02-17T05:38:44.000Z","dependencies_parsed_at":null,"dependency_job_id":"ef2c7f17-f629-4444-aeb2-156ce7df0785","html_url":"https://github.com/geekskai/react-ts-tetris","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geekskai%2Freact-ts-tetris","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geekskai%2Freact-ts-tetris/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geekskai%2Freact-ts-tetris/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/geekskai%2Freact-ts-tetris/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/geekskai","download_url":"https://codeload.github.com/geekskai/react-ts-tetris/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":241391053,"owners_count":19955466,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-11T14:48:11.744Z","updated_at":"2026-05-15T05:36:02.581Z","avatar_url":"https://github.com/geekskai.png","language":"JavaScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"### English introduction\n\nPlease view [README-EN.md](https://github.com/chvin/react-tetris/blob/master/README-EN.md)\n\n---\n\n## 用 React、Redux、Immutable 做俄罗斯方块\n\n---\n\n俄罗斯方块是一直各类程序语言热衷实现的经典游戏，JavaScript 的实现版本也有很多，用 React 做好俄罗斯方块则成了我一个目标。\n\n戳：[https://chvin.github.io/react-tetris/](https://chvin.github.io/react-tetris/) 玩一玩！\n\n---\n\n### 效果预览\n\n![效果预览](https://img.alicdn.com/tps/TB1Ag7CNXXXXXaoXXXXXXXXXXXX-320-483.gif)\n\n正常速度的录制，体验流畅。\n\n### 响应式\n\n![响应式](https://img.alicdn.com/tps/TB1AdjZNXXXXXcCapXXXXXXXXXX-480-343.gif)\n\n不仅指屏幕的自适应，而是`在PC使用键盘、在手机使用手指的响应式操作`：\n\n![手机](https://img.alicdn.com/tps/TB1kvJyOVXXXXbhaFXXXXXXXXXX-320-555.gif)\n\n### 数据持久化\n\n![数据持久化](https://img.alicdn.com/tps/TB1EY7cNXXXXXXraXXXXXXXXXXX-320-399.gif)\n\n玩单机游戏最怕什么？断电。通过订阅 `store.subscribe`，将 state 储存在 localStorage，精确记录所有状态。网页关了刷新了、程序崩溃了、手机没电了，重新打开连接，都可以继续。\n\n### Redux 状态预览（[Redux DevTools extension](https://github.com/zalmoxisus/redux-devtools-extension)）\n\n![Redux状态预览](https://img.alicdn.com/tps/TB1hGQqNXXXXXX3XFXXXXXXXXXX-640-381.gif)\n\nRedux 设计管理了所有应存的状态，这是上面持久化的保证。\n\n---\n\n游戏框架使用的是 React + Redux，其中再加入了 Immutable，用它的实例来做来 Redux 的 state。（有关 React 和 Redux 的介绍可以看：[React 入门实例](http://www.ruanyifeng.com/blog/2015/03/react.html)、[Redux 中文文档](https://camsong.github.io/redux-in-chinese/index.html)）\n\n## 1、什么是 Immutable？\n\nImmutable 是一旦创建，就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。\n\n### 初识：\n\n让我们看下面一段代码：\n\n```JavaScript\nfunction keyLog(touchFn) {\n  let data = { key: 'value' };\n  f(data);\n  console.log(data.key); // 猜猜会打印什么？\n}\n```\n\n不查看 f，不知道它对 `data` 做了什么，无法确认会打印什么。但如果 `data` 是 Immutable，你可以确定打印的是 `value`：\n\n```JavaScript\nfunction keyLog(touchFn) {\n  let data = Immutable.Map({ key: 'value' });\n  f(data);\n  console.log(data.get('key'));  // value\n}\n```\n\nJavaScript 中的`Object`与`Array`等使用的是引用赋值，新的对象简单的引用了原始对象，改变新也将影响旧的：\n\n```JavaScript\nfoo = {a: 1};  bar = foo;  bar.a = 2;\nfoo.a // 2\n```\n\n虽然这样做可以节约内存，但当应用复杂后，造成了状态不可控，是很大的隐患，节约的内存优点变得得不偿失。\n\nImmutable 则不一样，相应的：\n\n```JavaScript\nfoo = Immutable.Map({ a: 1 });  bar = foo.set('a', 2);\nfoo.get('a') // 1\n```\n\n### 简洁：\n\n在`Redux`中，它的最优做法是每个`reducer`都返回一个新的对象（数组），所以我们常常会看到这样的代码：\n\n```JavaScript\n// reducer\n...\nreturn [\n   ...oldArr.slice(0, 3),\n   newValue,\n   ...oldArr.slice(4)\n];\n```\n\n为了返回新的对象（数组），不得不有上面奇怪的样子，而在使用更深的数据结构时会变的更棘手。\n让我们看看 Immutable 的做法：\n\n```JavaScript\n// reducer\n...\nreturn oldArr.set(4, newValue);\n```\n\n是不是很简洁？\n\n### 关于 “===”：\n\n我们知道对于`Object`与`Array`的`===`比较，是对引用地址的比较而不是“值比较”，如：\n\n```JavaScript\n{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false\n[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false\n```\n\n对于上面只能采用 `deepCopy`、`deepCompare`来遍历比较，不仅麻烦且好性能。\n\n我们感受来一下`Immutable`的做法！\n\n```JavaScript\nmap1 = Immutable.Map({a:1, b:2, c:3});\nmap2 = Immutable.Map({a:1, b:2, c:3});\nImmutable.is(map1, map2); // true\n\n// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);\nList1 = Immutable.fromJS([1, 2, [3, 4]]);\nList2 = Immutable.fromJS([1, 2, [3, 4]]);\nImmutable.is(List1, List2); // true\n```\n\n似乎有阵清风吹过。\n\nReact 做性能优化时有一个`大招`，就是使用 `shouldComponentUpdate()`，但它默认返回 `true`，即始终会执行 `render()` 方法，后面做 Virtual DOM 比较。\n\n在使用原生属性时，为了得出 shouldComponentUpdate 正确的`true` or `false`，不得不用 deepCopy、deepCompare 来算出答案，消耗的性能很不划算。而在有了 Immutable 之后，使用上面的方法对深层结构的比较就变的易如反掌。\n\n对于「俄罗斯方块」，试想棋盘是一个`二维数组`，可以移动的方块则是`形状(也是二维数组)`+`坐标`。棋盘与方块的叠加则组成了最后的结果`Matrix`。游戏中上面的属性都由`Immutable`构建，通过它的比较方法，可以轻松写好`shouldComponentUpdate`。源代码：[/src/components/matrix/index.js#L35](https://github.com/chvin/react-tetris/blob/master/src/components/matrix/index.js#L35)\n\nImmutable 学习资料：\n\n- [Immutable.js](http://facebook.github.io/immutable-js/)\n- [Immutable 详解及 React 中实践](https://github.com/camsong/blog/issues/3)\n\n---\n\n## 2、如何在 Redux 中使用 Immutable\n\n目标：将`state` -\u003e Immutable 化。\n关键的库：[gajus/redux-immutable](https://github.com/gajus/redux-immutable)\n将原来 Redux 提供的 combineReducers 改由上面的库提供：\n\n```JavaScript\n// rootReducers.js\n// import { combineReducers } from 'redux'; // 旧的方法\nimport { combineReducers } from 'redux-immutable'; // 新的方法\n\nimport prop1 from './prop1';\nimport prop2 from './prop2';\nimport prop3 from './prop3';\n\nconst rootReducer = combineReducers({\n  prop1, prop2, prop3,\n});\n\n\n// store.js\n// 创建store的方法和常规一样\nimport { createStore } from 'redux';\nimport rootReducer from './reducers';\n\nconst store = createStore(rootReducer);\nexport default store;\n```\n\n通过新的`combineReducers`将把 store 对象转化成 Immutable，在 container 中使用时也会略有不同（但这正是我们想要的）：\n\n```JavaScript\nconst mapStateToProps = (state) =\u003e ({\n  prop1: state.get('prop1'),\n  prop2: state.get('prop2'),\n  prop3: state.get('prop3'),\n  next: state.get('next'),\n});\nexport default connect(mapStateToProps)(App);\n```\n\n---\n\n## 3、Web Audio Api\n\n游戏里有很多不同的音效，而实际上只引用了一个音效文件：[/build/music.mp3](https://github.com/chvin/react-tetris/blob/master/build/music.mp3)。借助`Web Audio Api`能够以毫秒级精确、高频率的播放音效，这是`\u003caudio\u003e`标签所做不到的。在游戏进行中按住方向键移动方块，便可以听到高频率的音效。\n\n![网页音效进阶](https://img.alicdn.com/tps/TB1fYgzNXXXXXXnXpXXXXXXXXXX-633-358.png)\n\n`WAA` 是一套全新的相对独立的接口系统，对音频文件拥有更高的处理权限以及更专业的内置音频效果，是 W3C 的推荐接口，能专业处理“音速、音量、环境、音色可视化、高频、音向”等需求，下图介绍了 WAA 的使用流程。\n\n![流程](https://img.alicdn.com/tps/TB1nBf1NXXXXXagapXXXXXXXXXX-520-371.png)\n\n其中 Source 代表一个音频源，Destination 代表最终的输出，多个 Source 合成出了 Destination。\n源代码：[/src/unit/music.js](https://github.com/chvin/react-tetris/blob/master/src/unit/music.js) 实现了 ajax 加载 mp3，并转为 WAA，控制播放的过程。\n\n`WAA` 在各个浏览器的最新 2 个版本下的支持情况（[CanIUse](http://caniuse.com/#search=webaudio)）\n\n![浏览器兼容](https://img.alicdn.com/tps/TB15z4VOVXXXXahaXXXXXXXXXXX-679-133.png)\n\n可以看到 IE 阵营与大部分安卓机不能使用，其他 ok。\n\nWeb Audio Api 学习资料：\n\n- [Web API 接口| MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API)\n- [Getting Started with Web Audio API](http://www.html5rocks.com/en/tutorials/webaudio/intro/)\n\n---\n\n## 4、游戏在体验上的优化\n\n- 技术：\n  - 按下方向键水平移动和竖直移动的触发频率是不同的，游戏可以定义触发频率，代替原生的事件频率，源代码：[/src/unit/event.js](https://github.com/chvin/react-tetris/blob/master/src/unit/event.js) ；\n  - 左右移动可以 delay 掉落的速度，但在撞墙移动的时候 delay 的稍小；在速度为 6 级时 通过 delay 会保证在一行内水平完整移动一次；\n  - 对按钮同时注册`touchstart`和`mousedown`事件，以供响应式游戏。当`touchstart`发生时，不会触发`mousedown`，而当`mousedown`发生时，由于鼠标移开事件元素可以不触发`mouseup`，将同时监听`mouseout` 模拟 `mouseup`。源代码：[/src/components/keyboard/index.js](https://github.com/chvin/react-tetris/blob/master/src/components/keyboard/index.js)；\n  - 监听了 `visibilitychange` 事件，当页面被隐藏\\切换的时候，游戏将不会进行，切换回来将继续，这个`focus`状态也被写进了 Redux 中。所以当用手机玩来`电话`时，游戏进度将保存；PC 开着游戏干别的也不会听到 gameover，这有点像 `ios` 应用的切换。\n  - 在`任意`时刻刷新网页，（比如消除方块时、游戏结束时）也能还原当前状态；\n  - 游戏中唯一用到的图片是![image](https://img.alicdn.com/tps/TB1qq7kNXXXXXacXFXXXXXXXXXX-400-186.png)，其他都是 CSS；\n  - 游戏兼容 Chrome、Firefox、IE9+、Edge 等；\n- 玩法：\n  - 可以在游戏未开始时制定初始的棋盘（十个级别）和速度（六个级别）；\n  - 一次消除 1 行得 100 分、2 行得 300 分、3 行得 700 分、4 行得 1500 分；\n  - 方块掉落速度会随着消除的行数增加（每 20 行增加一个级别）；\n\n---\n\n## 5、开发中的经验梳理\n\n- 为所有的`component`都编写了`shouldComponentUpdate`，在手机上的性能相对有显著的提升。中大型应用在遇到性能上的问题的时候，写好 shouldComponentUpdate 一定会帮你一把。\n- `无状态组件`（[Stateless Functional Components](https://medium.com/@joshblack/stateless-components-in-react-0-14-f9798f8b992d#.xjqnbfx4e)）是没有生命周期的。而因为上条因素，所有组件都需要生命周期 shouldComponentUpdate，所以未使用无状态组件。\n- 在 `webpack.config.js` 中的 devServer 属性写入`host: '0.0.0.0'`，可以在开发时用 ip 访问，不局限在 localhost；\n- redux 中的`store`并非只能通过 connect 将方法传递给`container`，可以跳出组件，在别的文件拿出来做流程控制(dispatch)，源代码：[/src/control/states.js](https://github.com/chvin/react-tetris/blob/master/src/control/states.js)；\n- 用 react+redux 做持久化非常的方便，只要将 redux 状态储存，在每一个 reduers 做初始化的时候读取就好。\n- 通过配置 .eslintrc.js` 与 webpack.config.js` ，项目中集成了 `ESLint` 检验。使用 ESLint 可以使编码按规范编写，有效地控制代码质量。不符规范的代码在开发时（或 build 时）都能通过 IDE 与控制台发现错误。 参考：[Airbnb: React 使用规范](https://github.com/dwqs/react-style-guide)；\n\n---\n\n## 6、总结\n\n- 作为一个 React 的练手应用，在实现的过程中发现小小的“方块”还是有很多的细节可以优化和打磨，这时就是考验一名前端工程师的细心和功力的时候。\n- 优化的方向既有 React 的本身，比如哪些状态由 Redux 存，哪些状态给组件的 state 就好；而跳出框架又有产品的很多特点可以玩，为了达到你的需求，这些都将自然的推进技术的发展。\n- 一个项目从零开始，功能一点一滴慢慢累积，就会盖成高楼，不要畏难，有想法就敲起来吧。 ^\\_^\n\n---\n\n## 7、控制流程\n\n![控制流程](https://img.alicdn.com/tfs/TB1B6ODRXXXXXXHaFXXXXXXXXXX-1920-1080.png)\n\n---\n\n## 8、开发\n\n### 安装\n\n```\nnpm install\n```\n\n### 运行\n\n```\nnpm start\n```\n\n浏览自动打开 [http://127.0.0.1:8080/](http://127.0.0.1:8080/)\n\n### 多语言\n\n在 [i18n.json](https://github.com/chvin/react-tetris/blob/master/i18n.json) 配置多语言环境，使用\"lan\"参数匹配语言如：`https://chvin.github.io/react-tetris/?lan=en`\n\n### 打包编译\n\n```\nnpm run build\n```\n\n在 build 文件夹下生成结果。\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeekskai%2Freact-ts-tetris","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fgeekskai%2Freact-ts-tetris","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fgeekskai%2Freact-ts-tetris/lists"}