vlambda博客
学习文章列表

用 React、Redux、Immutable 做俄罗斯方块

长按二维码试玩

用 React、Redux、Immutable 做俄罗斯方块


效果预览


用 React、Redux、Immutable 做俄罗斯方块
效果预览


正常速度的录制,体验流畅。

响应式


用 React、Redux、Immutable 做俄罗斯方块
响应式


不仅指屏幕的自适应,而是在PC使用键盘、在手机使用手指的响应式操作


用 React、Redux、Immutable 做俄罗斯方块
手机


数据持久化


用 React、Redux、Immutable 做俄罗斯方块
数据持久化


玩单机游戏最怕什么?断电。通过订阅 store.subscribe,将 state 储存在 localStorage,精确记录所有状态。网页关了刷新了、程序崩溃了、手机没电了,重新打开连接,都可以继续。

Redux 状态预览(Redux DevTools extension


用 React、Redux、Immutable 做俄罗斯方块
Redux状态预览


Redux 设计管理了所有应存的状态,这是上面持久化的保证。


游戏框架使用的是 React + Redux,其中再加入了 Immutable,用它的实例来做来 Redux 的 state。(有关 React 和 Redux 的介绍可以看:React 入门实例Redux 中文文档

1、什么是 Immutable?

Immutable 是一旦创建,就不能再被更改的数据。对 Immutable 对象的任何修改或添加删除操作都会返回一个新的 Immutable 对象。

初识:

让我们看下面一段代码:

function keyLog(touchFn) {
let data = { key: 'value' };
f(data);
console.log(data.key); // 猜猜会打印什么?
}

不查看 f,不知道它对 data 做了什么,无法确认会打印什么。但如果 data 是 Immutable,你可以确定打印的是 value

function keyLog(touchFn) {
let data = Immutable.Map({ key: 'value' });
f(data);
console.log(data.get('key')); // value
}

JavaScript 中的ObjectArray等使用的是引用赋值,新的对象简单的引用了原始对象,改变新也将影响旧的:

foo = {a: 1};  bar = foo;  bar.a = 2;
foo.a // 2

虽然这样做可以节约内存,但当应用复杂后,造成了状态不可控,是很大的隐患,节约的内存优点变得得不偿失。

Immutable 则不一样,相应的:

foo = Immutable.Map({ a: 1 });  bar = foo.set('a', 2);
foo.get('a') // 1

简洁:

Redux中,它的最优做法是每个reducer都返回一个新的对象(数组),所以我们常常会看到这样的代码:

// reducer
...
return [
...oldArr.slice(0, 3),
newValue,
...oldArr.slice(4)
];

为了返回新的对象(数组),不得不有上面奇怪的样子,而在使用更深的数据结构时会变的更棘手。让我们看看 Immutable 的做法:

// reducer
...
return oldArr.set(4, newValue);

是不是很简洁?

关于 “===”:

{a:1, b:2, c:3} === {a:1, b:2, c:3}; // false
[1, 2, [3, 4]] === [1, 2, [3, 4]]; // false

对于上面只能采用 deepCopydeepCompare来遍历比较,不仅麻烦且好性能。

我们感受来一下Immutable的做法!

map1 = Immutable.Map({a:1, b:2, c:3});
map2 = Immutable.Map({a:1, b:2, c:3});
Immutable.is(map1, map2); // true

// List1 = Immutable.List([1, 2, Immutable.List[3, 4]]);
List1 = Immutable.fromJS([1, 2, [3, 4]]);
List2 = Immutable.fromJS([1, 2, [3, 4]]);
Immutable.is(List1, List2); // true

似乎有阵清风吹过。

React 做性能优化时有一个大招,就是使用 shouldComponentUpdate(),但它默认返回 true,即始终会执行 render() 方法,后面做 Virtual DOM 比较。

在使用原生属性时,为了得出 shouldComponentUpdate 正确的true or false,不得不用 deepCopy、deepCompare 来算出答案,消耗的性能很不划算。而在有了 Immutable 之后,使用上面的方法对深层结构的比较就变的易如反掌。

对于「俄罗斯方块」,试想棋盘是一个二维数组,可以移动的方块则是形状(也是二维数组)+坐标。棋盘与方块的叠加则组成了最后的结果Matrix。游戏中上面的属性都由Immutable构建,通过它的比较方法,可以轻松写好shouldComponentUpdate。源代码:/src/components/matrix/index.js#L35

Immutable 学习资料:

  • Immutable.js

  • Immutable 详解及 React 中实践


2、如何在 Redux 中使用 Immutable

目标:将state -> Immutable 化。关键的库:gajus/redux-immutable将原来 Redux 提供的 combineReducers 改由上面的库提供:

// rootReducers.js
// import { combineReducers } from 'redux'; // 旧的方法
import { combineReducers } from 'redux-immutable'; // 新的方法

import prop1 from './prop1';
import prop2 from './prop2';
import prop3 from './prop3';

const rootReducer = combineReducers({
prop1, prop2, prop3,
});


// store.js
// 创建store的方法和常规一样
import { createStore } from 'redux';
import rootReducer from './reducers';

const store = createStore(rootReducer);
export default store;

通过新的combineReducers将把 store 对象转化成 Immutable,在 container 中使用时也会略有不同(但这正是我们想要的):

const mapStateToProps = (state) => ({
prop1: state.get('prop1'),
prop2: state.get('prop2'),
prop3: state.get('prop3'),
next: state.get('next'),
});
export default connect(mapStateToProps)(App);

3、Web Audio Api

游戏里有很多不同的音效,而实际上只引用了一个音效文件:/build/music.mp3。借助Web Audio Api能够以毫秒级精确、高频率的播放音效,这是<audio>标签所做不到的。在游戏进行中按住方向键移动方块,便可以听到高频率的音效。


用 React、Redux、Immutable 做俄罗斯方块
网页音效进阶


WAA 是一套全新的相对独立的接口系统,对音频文件拥有更高的处理权限以及更专业的内置音频效果,是 W3C 的推荐接口,能专业处理“音速、音量、环境、音色可视化、高频、音向”等需求,下图介绍了 WAA 的使用流程。


用 React、Redux、Immutable 做俄罗斯方块
流程


其中 Source 代表一个音频源,Destination 代表最终的输出,多个 Source 合成出了 Destination。源代码:/src/unit/music.js 实现了 ajax 加载 mp3,并转为 WAA,控制播放的过程。

WAA 在各个浏览器的最新 2 个版本下的支持情况(CanIUse


用 React、Redux、Immutable 做俄罗斯方块
浏览器兼容


可以看到 IE 阵营与大部分安卓机不能使用,其他 ok。

Web Audio Api 学习资料:

  • Web API 接口| MDN

  • Getting Started with Web Audio API


4、游戏在体验上的优化

  • 技术:

    • 按下方向键水平移动和竖直移动的触发频率是不同的,游戏可以定义触发频率,代替原生的事件频率,源代码:/src/unit/event.js

    • 左右移动可以 delay 掉落的速度,但在撞墙移动的时候 delay 的稍小;在速度为 6 级时 通过 delay 会保证在一行内水平完整移动一次;

    • 对按钮同时注册touchstartmousedown事件,以供响应式游戏。当touchstart发生时,不会触发mousedown,而当mousedown发生时,由于鼠标移开事件元素可以不触发mouseup,将同时监听mouseout 模拟 mouseup。源代码:/src/components/keyboard/index.js

    • 任意时刻刷新网页,(比如消除方块时、游戏结束时)也能还原当前状态;

    • 游戏中唯一用到的图片是

      image

      ,其他都是 CSS;

    • 游戏兼容 Chrome、Firefox、IE9+、Edge 等;

  • 玩法:

    • 可以在游戏未开始时制定初始的棋盘(十个级别)和速度(六个级别);

    • 一次消除 1 行得 100 分、2 行得 300 分、3 行得 700 分、4 行得 1500 分;

    • 方块掉落速度会随着消除的行数增加(每 20 行增加一个级别);


5、开发中的经验梳理

  • 为所有的component都编写了shouldComponentUpdate,在手机上的性能相对有显著的提升。中大型应用在遇到性能上的问题的时候,写好 shouldComponentUpdate 一定会帮你一把。

  • 无状态组件Stateless Functional Components)是没有生命周期的。而因为上条因素,所有组件都需要生命周期 shouldComponentUpdate,所以未使用无状态组件。

  • webpack.config.js 中的 devServer 属性写入host: '0.0.0.0',可以在开发时用 ip 访问,不局限在 localhost;

  • redux 中的store并非只能通过 connect 将方法传递给container,可以跳出组件,在别的文件拿出来做流程控制(dispatch),源代码:/src/control/states.js

  • 用 react+redux 做持久化非常的方便,只要将 redux 状态储存,在每一个 reduers 做初始化的时候读取就好。

  • 通过配置 .eslintrc.js 与 webpack.config.js ,项目中集成了 ESLint 检验。使用 ESLint 可以使编码按规范编写,有效地控制代码质量。不符规范的代码在开发时(或 build 时)都能通过 IDE 与控制台发现错误。参考:Airbnb: React 使用规范


6、总结

  • 作为一个 React 的练手应用,在实现的过程中发现小小的“方块”还是有很多的细节可以优化和打磨,这时就是考验一名前端工程师的细心和功力的时候。

  • 优化的方向既有 React 的本身,比如哪些状态由 Redux 存,哪些状态给组件的 state 就好;而跳出框架又有产品的很多特点可以玩,为了达到你的需求,这些都将自然的推进技术的发展。

  • 一个项目从零开始,功能一点一滴慢慢累积,就会盖成高楼,不要畏难,有想法就敲起来吧。^_^


7、控制流程


控制流程



8、开发

安装

npm install

运行

npm start

浏览自动打开 http://127.0.0.1:8080/

多语言

i18n.json 配置多语言环境,使用"lan"参数匹配语言如:https://chvin.github.io/react-tetris/?lan=en

打包编译

npm run build

在 build 文件夹下生成结果。