俄羅斯方塊是一直各類程式語言熱衷實現的經典遊戲,JavsScript的實現版本也有很多,用React 做好俄羅斯方塊則成了我一個目標。
戳:chvin.github.io/react-tetri… 玩一玩!
效果預覽
正常速度的錄製,體驗流暢。
響應式
不僅指螢幕的自適應,而是在PC使用鍵盤、在手機使用手指的響應式操作
:
資料持久化
玩單機遊戲最怕什麼?斷電。通過訂閱 store.subscribe
,將state儲存在localStorage,精確記錄所有狀態。網頁關了重新整理了、程式崩潰了、手機沒電了,重新開啟連線,都可以繼續。
Redux 狀態預覽(Redux DevTools extension)
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學習資料:
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 學習資料:
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就好;而跳出框架又有產品的很多特點可以玩,為了達到你的需求,這些都將自然的推進技術的發展。
- 一個專案從零開始,功能一點一滴慢慢累積,就會蓋成高樓,不要畏難,有想法就敲起來吧。 ^_^
7、控制流程
這篇文章參加掘金技術徵文:gold.xitu.io/post/58522d…