用React、Redux、Immutable做俄羅斯方塊

俄羅斯方塊是一直各類程序語言熱衷實現的經典遊戲,JavsScript的實現版本也有很多,用React 做好俄羅斯方塊則成了我一個目標。

戳:俄羅斯方塊 玩一玩!

開源地址:https://github.com/chvin/react-tetris/

效果預覽:

好吧,知乎文章不支持gif,不過話說右鍵打開新圖片,然後把圖片格式改為gif就動起來了…… 也可以直接去github瀏覽

正常速度的錄製,體驗流暢。

響應式:

不僅指屏幕的自適應,而是在PC使用鍵盤、在手機使用手指的響應式操作:

數據持久化

玩單機遊戲最怕什麼?斷電。通過訂閱 store.subscribe,將state儲存在localStorage,精確記錄所有狀態。網頁關了刷新了、程序崩潰了、手機沒電了,重新打開連接,都可以繼續。

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 中的Object與Array等使用的是引用賦值,新的對象簡單的引用了原始對象,改變新也將影響舊的:

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);

是不是很簡潔?

關於 「===」:

我們知道對於Object與Array的===比較,是對引用地址的比較而不是「值比較」,如:

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

對於上面只能採用 deepCopy、deepCompare`來遍歷比較,不僅麻煩且好性能。

我們感受來一下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改由上面的庫提供:

// rootReduers.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>標籤所做不到的。在遊戲進行中按住方向鍵移動方塊,便可以聽到高頻率的音效。

WAA 是一套全新的相對獨立的介面系統,對音頻文件擁有更高的處理許可權以及更專業的內置音頻效果,是W3C的推薦介面,能專業處理「音速、音量、環境、音色可視化、高頻、音向」等需求,下圖介紹了WAA的使用流程。

其中Source代表一個音頻源,Destination代表最終的輸出,多個Source合成出了Destination。

源代碼:/src/unit/music.js 實現了ajax載入mp3,並轉為WAA,控制播放的過程。

WAA 在各個瀏覽器的最新2個版本下的支持情況(CanIUse)

可以看到IE陣營與大部分安卓機不能使用,其他ok。

Web Audio Api 學習資料:

  • Web API 介面| MDN
  • Getting Started with Web Audio API

---------------------------------

4、遊戲在體驗上的優化

  • 技術:
    • 按下方向鍵水平移動和豎直移動的觸發頻率是不同的,遊戲可以定義觸發頻率,代替原生的事件頻率,源代碼:/src/unit/event.js ;
    • 左右移動可以 delay 掉落的速度,但在撞牆移動的時候 delay 的稍小;在速度為6級時 通過delay 會保證在一行內水平完整移動一次;
    • 對按鈕同時註冊touchstart和mousedown事件,以供響應式遊戲。當touchstart發生時,不會觸發mousedown,而當mousedown發生時,由於滑鼠移開事件元素可以不觸發mouseup,將同時監聽mouseout 模擬 mouseup`。源代碼:/src/components/keyboard/index.js;
    • 監聽了 visibilitychange事件,當頁面被隱藏切換的時候,遊戲將不會進行,切換回來將繼續,這個focus狀態也被寫進了Redux中。所以當用手機玩來電話時,遊戲進度將保存;PC開著遊戲干別的也不會聽到gameover,這有點像ios` 應用的切換。
    • 在任意時刻刷新網頁,(比如消除方塊時、遊戲結束時)也能還原當前狀態;
    • 遊戲中唯一用到的圖片是

      ,其他都是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就好;而跳出框架又有產品的很多特點可以玩,為了達到你的需求,這些都將自然的推進技術的發展。
  • 一個項目從零開始,功能一點一滴慢慢累積,就會蓋成高樓,不要畏難,有想法就敲起來吧。 ^_^

推薦閱讀:

React 16.2.x全新全譯
redux 中的 state 樹太大會不會有性能問題?
從0實現一個tinyredux
寫在2017的前端數據層不完全指北
Redux-Saga 初識和總結

TAG:前端開發 | React | Redux |