|
1 |
| -# vue-tetris |
2 |
| -Use Vue, Vuex, Immutable to code Tetris. |
| 1 | +---- |
| 2 | +## 用Vue、Vuex、Immutable做俄罗斯方块 |
| 3 | + |
| 4 | +---- |
| 5 | +本项目灵感来源于 React 版的俄罗斯方块,由于对其实现原理较感兴趣,而且相比于 React 更喜欢 Vue, 于是把 React 版的重构为了 Vue 版的,大致思路是把组件当成一个个函数,保证一个输入(props)能得到一个确定的输出(view),然后对不同方法也是做同样处理,对于 Redux 使用 Vuex 精简化 |
| 6 | + |
| 7 | +戳:[http://binaryify.github.io/vue-tetris/](http://binaryify.github.io/vue-tetris/) 玩一玩! |
| 8 | + |
| 9 | +---- |
| 10 | +### 效果预览 |
| 11 | + |
| 12 | + |
| 13 | +正常速度的录制,体验流畅。 |
| 14 | + |
| 15 | +### 响应式 |
| 16 | + |
| 17 | + |
| 18 | +不仅指屏幕的自适应,而是`在PC使用键盘、在手机使用手指的响应式操作`: |
| 19 | + |
| 20 | + |
| 21 | + |
| 22 | +### 数据持久化 |
| 23 | + |
| 24 | + |
| 25 | +玩单机游戏最怕什么?断电。通过订阅 `store.subscribe`,将state储存在localStorage,精确记录所有状态。网页关了刷新了、程序崩溃了、手机没电了,重新打开连接,都可以继续。 |
| 26 | + |
| 27 | +### Vuex 状态预览([Vue DevTools extension](https://github.com/vuejs/vue-devtools)) |
| 28 | + |
| 29 | + |
| 30 | +Vuex 设计管理了所有应存的状态,这是上面持久化的保证。 |
| 31 | + |
| 32 | +---- |
| 33 | +游戏框架使用的是 Vue + Vuex,其中再加入了 Immutable,确保性能和数据可靠性 |
| 34 | + |
| 35 | + |
| 36 | +## 1、什么是 Immutable? |
| 37 | +Immutable 是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。 |
| 38 | + |
| 39 | +### 初识: |
| 40 | +让我们看下面一段代码: |
| 41 | +``` JavaScript |
| 42 | +function keyLog(touchFn) { |
| 43 | + let data = { key: 'value' }; |
| 44 | + f(data); |
| 45 | + console.log(data.key); // 猜猜会打印什么? |
| 46 | +} |
| 47 | +``` |
| 48 | +不查看f,不知道它对 `data` 做了什么,无法确认会打印什么。但如果 `data` 是 Immutable,你可以确定打印的是 `value`: |
| 49 | +``` JavaScript |
| 50 | +function keyLog(touchFn) { |
| 51 | + let data = Immutable.Map({ key: 'value' }); |
| 52 | + f(data); |
| 53 | + console.log(data.get('key')); // value |
| 54 | +} |
| 55 | +``` |
| 56 | + |
| 57 | +JavaScript 中的`Object`与`Array`等使用的是引用赋值,新的对象简单的引用了原始对象,改变新也将影响旧的: |
| 58 | +``` JavaScript |
| 59 | +foo = {a: 1}; bar = foo; bar.a = 2; |
| 60 | +foo.a // 2 |
| 61 | +``` |
| 62 | +虽然这样做可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约的内存优点变得得不偿失。 |
| 63 | + |
| 64 | +Immutable则不一样,相应的: |
| 65 | +``` JavaScript |
| 66 | +foo = Immutable.Map({ a: 1 }); bar = foo.set('a', 2); |
| 67 | +foo.get('a') // 1 |
| 68 | +``` |
| 69 | + |
| 70 | +### 关于 “===”: |
| 71 | +我们知道对于`Object`与`Array`的`===`比较,是对引用地址的比较而不是“值比较”,如: |
| 72 | +``` JavaScript |
| 73 | +{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false |
| 74 | +[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false |
| 75 | +``` |
| 76 | +对于上面只能采用 `deepCopy`、`deepCompare`来遍历比较,不仅麻烦且好性能。 |
| 77 | + |
| 78 | +我们感受来一下`Immutable`的做法! |
| 79 | +``` JavaScript |
| 80 | +map1 = Immutable.Map({a:1, b:2, c:3}); |
| 81 | +map2 = Immutable.Map({a:1, b:2, c:3}); |
| 82 | +Immutable.is(map1, map2); // true |
| 83 | + |
| 84 | +// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]); |
| 85 | +List1 = Immutable.fromJS([1, 2, [3, 4]]); |
| 86 | +List2 = Immutable.fromJS([1, 2, [3, 4]]); |
| 87 | +Immutable.is(List1, List2); // true |
| 88 | +``` |
| 89 | + |
| 90 | + |
| 91 | +Immutable学习资料: |
| 92 | +* [Immutable.js](http://facebook.github.io/immutable-js/) |
| 93 | + |
| 94 | + |
| 95 | +## 2、Web Audio Api |
| 96 | +游戏里有很多不同的音效,而实际上只引用了一个音效文件:[/build/music.mp3](https://github.com/chvin/react-tetris/blob/master/build/music.mp3)。借助`Web Audio Api`能够以毫秒级精确、高频率的播放音效,这是`<audio>`标签所做不到的。在游戏进行中按住方向键移动方块,便可以听到高频率的音效。 |
| 97 | + |
| 98 | + |
| 99 | + |
| 100 | +`WAA` 是一套全新的相对独立的接口系统,对音频文件拥有更高的处理权限以及更专业的内置音频效果,是W3C的推荐接口,能专业处理“音速、音量、环境、音色可视化、高频、音向”等需求,下图介绍了WAA的使用流程。 |
| 101 | + |
| 102 | + |
| 103 | + |
| 104 | +其中Source代表一个音频源,Destination代表最终的输出,多个Source合成出了Destination。 |
| 105 | +源代码:[/src/unit/music.js](https://github.com/chvin/react-tetris/blob/master/src/unit/music.js) 实现了ajax加载mp3,并转为WAA,控制播放的过程。 |
| 106 | + |
| 107 | +`WAA` 在各个浏览器的最新2个版本下的支持情况([CanIUse](http://caniuse.com/#search=webaudio)) |
| 108 | + |
| 109 | + |
| 110 | + |
| 111 | +可以看到IE阵营与大部分安卓机不能使用,其他ok。 |
| 112 | + |
| 113 | + |
| 114 | +Web Audio Api 学习资料: |
| 115 | +* [Web API 接口| MDN](https://developer.mozilla.org/zh-CN/docs/Web/API/Web_Audio_API) |
| 116 | +* [Getting Started with Web Audio API](http://www.html5rocks.com/en/tutorials/webaudio/intro/) |
| 117 | + |
| 118 | +---- |
| 119 | +## 3、游戏在体验上的优化 |
| 120 | +* 技术: |
| 121 | + * 按下方向键水平移动和竖直移动的触发频率是不同的,游戏可以定义触发频率,代替原生的事件频率,源代码:[/src/unit/event.js](https://github.com/chvin/react-tetris/blob/master/src/unit/event.js) ; |
| 122 | + * 左右移动可以 delay 掉落的速度,但在撞墙移动的时候 delay 的稍小;在速度为6级时 通过delay 会保证在一行内水平完整移动一次; |
| 123 | + * 对按钮同时注册`touchstart`和`mousedown`事件,以供响应式游戏。当`touchstart`发生时,不会触发`mousedown`,而当`mousedown`发生时,由于鼠标移开事件元素可以不触发`mouseup`,将同时监听`mouseout` 模拟 `mouseup`。源代码:[/src/components/keyboard/index.js](https://github.com/Binaryify/vue-tetris/blob/master/src/components/keyboard/index.js); |
| 124 | + * 监听了 `visibilitychange` 事件,当页面被隐藏\切换的时候,游戏将不会进行,切换回来将继续,这个`focus`状态也被写进了 Vuex 中。所以当用手机玩来`电话`时,游戏进度将保存;PC开着游戏干别的也不会听到gameover,这有点像 `ios` 应用的切换。 |
| 125 | + * 在`任意`时刻刷新网页,(比如消除方块时、游戏结束时)也能还原当前状态; |
| 126 | + * 游戏中唯一用到的图片是,其他都是CSS; |
| 127 | + * 游戏兼容 Chrome、Firefox、IE9+、Edge等; |
| 128 | +* 玩法: |
| 129 | + * 可以在游戏未开始时制定初始的棋盘(十个级别)和速度(六个级别); |
| 130 | + * 一次消除1行得100分、2行得300分、3行得700分、4行得1500分; |
| 131 | + * 方块掉落速度会随着消除的行数增加(每20行增加一个级别); |
| 132 | + |
| 133 | +---- |
| 134 | + |
| 135 | +## 4、开发中的经验梳理 |
| 136 | +Vue 版本和 React 版本核心代码基本相同,但在编写组件的时候遇到了几个问题,比如: |
| 137 | +1. React 版的 store 使用了 immutable 结构的数据,vuex 上的 store 如果使用了 immutable 结构,不利用监听数据变化,故把store 的数据全部使用了普通的数据,在需要这些数据的地方通过 immutable 提供的 `fromJS` 转换,在需要普通数据的地方再通过 immutable 的 `toJS` 转换成普通数据 |
| 138 | + |
| 139 | +2. Vue 没有 React 的`componentWillReceiveProps` 的生命周期,我的解决方法是使用 watch 配合 `deep:true` 来监听 props 的变化,如: |
| 140 | +```js |
| 141 | +watch: { |
| 142 | + $props: { |
| 143 | + deep: true, |
| 144 | + handler(nextProps) { |
| 145 | + //xxx |
| 146 | + } |
| 147 | + } |
| 148 | +} |
| 149 | +``` |
| 150 | + |
| 151 | +3. `matrix 组件` 的功能逻辑较复杂,使用 `template` 模版来渲染组件已经不合适了,通过自定义 render 方法再手动触发很繁琐,我的解决方法是通过 Vue 的 jsx 转换插件[babel-plugin-transform-vue-jsx](https://github.com/vuejs/babel-plugin-transform-vue-jsx)来使用 jsx 语法对页面进行渲染,当 props 或 state 变化了自动触发 render 方法,另外要注意的是 vue 的 jsx 和 React 的 jsx 书写上有一点的差异 |
| 152 | + |
| 153 | +## 5、架构差异 |
| 154 | +Redux 的数据流向是 把 store 的状态转化为 props 注入到 根组件,根组件再把这些 props 传入不同组件,当 store 的状态变化,根组件会重新 render, 更新子组件上的 props,子组件再 根据新 props重新 render |
| 155 | +引用知乎一个答友的回答[https://www.zhihu.com/question/47686258](https://www.zhihu.com/question/47686258)来说就是: |
| 156 | +>单例store的数据在react中可以通过view组件的属性(props)不断由父模块**“单向”**传递给子模块,形成一个树状分流结构。如果我们把redux比作整个应用的“心肺” (redux的flux功能像心脏,reducer功能像肺部毛细血管),那么这个过程可以比作心脏(store)将氧分子(数据)通过动脉毛细血管(props)送到各个器官组织(view组件)末端的view组件,又可以通过flux机制,将携带交互意图信息的action反馈给store。这个过程有点像将携带代谢产物的“红细胞”(action)通过静脉毛细血管又泵回心脏(store)action流回到store以后,action以参数的形式又被分流到各个具体的reducer组件中,这些reducer同样构成一个树状的hierarchy。这个过程像静脉血中的红细胞(action)被运输到肺部毛细血管(reducer组件)接收到action后,各个child reducer以返回值的形式,将最新的state返回给parent reducer,最终确保整个单例store的所有数据是最新的。这个过程可以比作肺部毛细血管的血液充氧后,又被重新泵回了心脏回到步骤1 |
| 157 | + |
| 158 | +而 vuex 的思路则不同,任何组件都随时可以通过 this.$store.state.xxx 获取 store 上的数据,更自由,只要 store 上的数据变了,组件都会自动重新渲染 |
| 159 | + |
| 160 | +## 6、开发 |
| 161 | +### 安装 |
| 162 | +``` |
| 163 | +npm install |
| 164 | +``` |
| 165 | +### 运行 |
| 166 | +``` |
| 167 | +npm run dev |
| 168 | +``` |
| 169 | +浏览自动打开 [localhost:8080](localhost:8080) |
| 170 | +### 多语言 |
| 171 | +在 [i18n.json](https://github.com/Binaryify/vue-tetris/blob/master/i18n.json) 配置多语言环境,使用"lan"参数匹配语言如:`https://Binaryify.github.io/vue-tetris/?lan=en` |
| 172 | +### 打包编译 |
| 173 | +``` |
| 174 | +npm run build |
| 175 | +``` |
| 176 | + |
| 177 | +在 `dist` 文件夹下生成结果。 |
0 commit comments