Redux 莞式教程 之 简明篇
Redux 簡明教程
原文鏈接(保持更新):https://github.com/kenberkele...
寫在前面
本教程深入淺出,配套 簡明教程、進(jìn)階教程(源碼精讀)以及文檔注釋豐滿的 Demo 等一條龍服務(wù)
§ 為什么要用 Redux
當(dāng)然還有 Flux、Reflux、Mobx 等狀態(tài)管理庫可供選擇
拋開需求講實用性都是耍流氓,因此下面由我扮演您那可親可愛的產(chǎn)品經(jīng)理
⊙ 需求 1:在控制臺上記錄用戶的每個動作
不知道您是否有后端的開發(fā)經(jīng)驗,后端一般會有記錄訪問日志的中間件
例如,在 Express 中實現(xiàn)一個簡單的 Logger 如下:
每次訪問的時候,都會在控制臺中留下類似下面的日志便于追蹤調(diào)試:
[Logger] GET / [Logger] POST /login [Logger] GET /user?uid=10086 ...如果我們把場景轉(zhuǎn)移到前端,請問該如何實現(xiàn)用戶的動作跟蹤記錄?
我們可能會這樣寫:
上述 jQuery 與 MV* 的寫法并沒有本質(zhì)上的區(qū)別
記錄用戶行為代碼的侵入性極強(qiáng),可維護(hù)性與擴(kuò)展性堪憂
⊙ 需求 2:在上述需求的基礎(chǔ)上,記錄用戶的操作時間
哼!最討厭就是改需求了,這種簡單的需求難道不是應(yīng)該一開始就想好的嗎?
呵呵,如果每位產(chǎn)品經(jīng)理都能一開始就把需求完善好,我們就不用加班了好伐
顯然地,前端的童鞋又得一個一個去改(當(dāng)然 編輯器 / IDE 都支持全局替換):
/** jQuery **/ $('#loginBtn').on('click', function(e) {console.log('[Logger] 用戶登錄', new Date())... }) $('#logoutBtn').on('click', function() {console.log('[Logger] 用戶退出登錄', new Date())... })/** MVC / MVVM 框架(這里以 Vue 舉例) **/ methods: {handleLogin () {console.log('[Logger] 用戶登錄', new Date())...},handleLogout () {console.log('[Logger] 用戶退出登錄', new Date())...} }而后端的童鞋只需要稍微修改一下原來的中間件即可:
var loggerMiddleware = function(req, res, next) {console.log('[Logger]', new Date(), req.method, req.originalUrl)next() } ... app.use(loggerMiddleware)⊙ 需求 3:正式上線的時候,把控制臺中有關(guān) Logger 的輸出全部去掉
難道您以為有了 UglifyJS,配置一個 drop_console: true 就好了嗎?圖樣圖森破,拿衣服!
請看清楚了,僅僅是去掉有關(guān) Logger 的 console.log,其他的要保留哦親~~~
于是前端的童鞋又不得不乖乖地一個一個注釋掉(當(dāng)然也可以設(shè)置一個環(huán)境變量判斷是否輸出,甚至可以重寫 console.log)
而我們后端的童鞋呢?只需要注釋掉一行代碼即可:// app.use(loggerMiddleware),真可謂是不費(fèi)吹灰之力
⊙ 需求 4:正式上線后,自動收集 bug,并還原出當(dāng)時的場景
收集用戶報錯還是比較簡單的,利用 window.error 事件,然后根據(jù) Source Map 定位到源碼(但一般查不出什么)
但要完全還原出當(dāng)時的使用場景,幾乎是不可能的。因為您不知道這個報錯,用戶是怎么一步一步操作得來的
就算知道用戶是如何操作得來的,但在您的電腦上,測試永遠(yuǎn)都是通過的(不是我寫的程序有問題,是用戶用的方式有問題)
相對地,后端的報錯的收集、定位以及還原卻是相當(dāng)簡單。只要一個 API 有 bug,那無論用什么設(shè)備訪問,都會得到這個 bug
還原 bug 也是相當(dāng)簡單:把數(shù)據(jù)庫備份導(dǎo)入到另一臺機(jī)器,部署同樣的運(yùn)行環(huán)境與代碼。如無意外,bug 肯定可以完美重現(xiàn)
在這個問題上拿后端跟前端對比,確實有失公允。但為了鼓吹 Redux 的優(yōu)越,只能勉為其難了
實際上 jQuery / MV* 中也能實現(xiàn)用戶動作的跟蹤,用一個數(shù)組往里面 push 用戶動作即可
但這樣操作的意義不大,因為僅僅只有動作,無法反映動作前后,應(yīng)用狀態(tài)的變動情況
※ 小結(jié)
為何前后端對于這類需求的處理竟然大相徑庭?后端為何可以如此優(yōu)雅?
原因在于,后端具有統(tǒng)一的入口與統(tǒng)一的狀態(tài)管理(數(shù)據(jù)庫),因此可以引入中間件機(jī)制來統(tǒng)一實現(xiàn)某些功能
多年來,前端工程師忍辱負(fù)重,操著賣白粉的心,賺著買白菜的錢,一直處于程序員鄙視鏈的底層
于是有大牛就把后端 MVC 的開發(fā)思維搬到前端,將應(yīng)用中所有的動作與狀態(tài)都統(tǒng)一管理,讓一切有據(jù)可循
使用 Redux,借助 Redux DevTools 可以實現(xiàn)出“華麗如時光旅行一般的調(diào)試效果”
實際上就是開發(fā)調(diào)試過程中可以撤銷與重做,并且支持應(yīng)用狀態(tài)的導(dǎo)入和導(dǎo)出(就像是數(shù)據(jù)庫的備份)
而且,由于可以使用日志完整記錄下每個動作,因此做到像 Git 般,隨時隨地恢復(fù)到之前的狀態(tài)
由于可以導(dǎo)出和導(dǎo)入應(yīng)用的狀態(tài)(包括路由狀態(tài)),因此還可以實現(xiàn)前后端同構(gòu)(服務(wù)端渲染)
當(dāng)然,既然有了動作日志以及動作前后的狀態(tài)備份,那么還原用戶報錯場景還會是一個難題嗎?
§ Store
首先要區(qū)分 store 和 state
state 是應(yīng)用的狀態(tài),一般本質(zhì)上是一個普通對象
例如,我們有一個 Web APP,包含 計數(shù)器 和 待辦事項 兩大功能
那么我們可以為該應(yīng)用設(shè)計出對應(yīng)的存儲數(shù)據(jù)結(jié)構(gòu)(應(yīng)用初始狀態(tài)):
store 是應(yīng)用狀態(tài) state 的管理者,包含下列四個函數(shù):
getState() # 獲取整個 state
dispatch(action) # ※ 觸發(fā) state 改變的【唯一途徑】※
subscribe(listener) # 您可以理解成是 DOM 中的 addEventListener
replaceReducer(nextReducer) # 一般在 Webpack Code-Splitting 按需加載的時候用
二者的關(guān)系是:state = store.getState()
Redux 規(guī)定,一個應(yīng)用只應(yīng)有一個單一的 store,其管理著唯一的應(yīng)用狀態(tài) state
Redux 還規(guī)定,不能直接修改應(yīng)用的狀態(tài) state,也就是說,下面的行為是不允許的:
若要改變 state,必須 dispatch 一個 action,這是修改應(yīng)用狀態(tài)的不二法門
現(xiàn)在您只需要記住 action 只是一個包含 type 屬性的普通對象即可
例如 { type: 'INCREMENT' }
上面提到,state 是通過 store.getState() 獲取,那么 store 又是怎么來的呢?
想生成一個 store,我們需要調(diào)用 Redux 的 createStore:
現(xiàn)在您只需要記住 reducer 是一個 函數(shù),負(fù)責(zé)更新并返回一個新的 state
而 initialState 主要用于前后端同構(gòu)的數(shù)據(jù)同步(詳情請關(guān)注 React 服務(wù)端渲染)
§ Action
上面提到,action(動作)實質(zhì)上是包含 type 屬性的普通對象,這個 type 是我們實現(xiàn)用戶行為追蹤的關(guān)鍵
例如,增加一個待辦事項 的 action 可能是像下面一樣:
當(dāng)然,action 的形式是多種多樣的,唯一的約束僅僅就是包含一個 type 屬性罷了
也就是說,下面這些 action 都是合法的:
雖說沒有約束,但最好還是遵循規(guī)范
如果需要新增一個代辦事項,實際上就是將 code-2 中的 payload “寫入” 到 state.todos 數(shù)組中(如何“寫入”?在此留個懸念):
/** 本代碼塊記為 code-3 **/ {counter: 0,todos: [{id: 1,content: '待辦事項1',completed: false}] }刨根問底,action 是誰生成的呢?
⊙ Action Creator
Action Creator 可以是同步的,也可以是異步的
顧名思義,Action Creator 是 action 的創(chuàng)造者,本質(zhì)上就是一個函數(shù),返回值是一個 action(對象)
例如下面就是一個 “新增一個待辦事項” 的 Action Creator:
將該函數(shù)應(yīng)用到一個表單(假設(shè) store 為全局變量,并引入了 jQuery ):
<--! 本代碼塊記為 code-5 --> <input type="text" id="todoInput" /> <button id="btn">提交</button><script> $('#btn').on('click', function() {var content = $('#todoInput').val() // 獲取輸入框的值var action = addTodo(content) // 執(zhí)行 Action Creator 獲得 actionstore.dispatch(action) // 改變 state 的不二法門:dispatch 一個 action!!! }) </script>在輸入框中輸入 “待辦事項2” 后,點擊一下提交按鈕,我們的 state 就變成了:
/** 本代碼塊記為 code-6 **/ {counter: 0,todos: [{id: 1,content: '待辦事項1',completed: false}, {id: 2,content: '待辦事項2',completed: false}] }通俗點講,Action Creator 用于綁定到用戶的操作(點擊按鈕等),其返回值 action 用于之后的 dispatch(action)
剛剛提到過,action 明明就沒有強(qiáng)制的規(guī)范,為什么 store.dispatch(action) 之后,
Redux 會明確知道是提取 action.payload,并且是對應(yīng)寫入到 state.todos 數(shù)組中?
又是誰負(fù)責(zé)“寫入”的呢?懸念即將揭曉...
§ Reducer
Reducer 必須是同步的純函數(shù)
用戶每次 dispatch(action) 后,都會觸發(fā) reducer 的執(zhí)行
reducer 的實質(zhì)是一個函數(shù),根據(jù) action.type 來更新 state 并返回 nextState
最后會用 reducer 的返回值 nextState 完全替換掉原來的 state
注意:上面的這個 “更新” 并不是指 reducer 可以直接對 state 進(jìn)行修改
Redux 規(guī)定,須先復(fù)制一份 state,在副本 nextState 上進(jìn)行修改操作
例如,可以使用 lodash 的 deepClone,也可以使用 Object.assign / map / filter/ ... 等返回副本的函數(shù)
在上面 Action Creator 中提到的 待辦事項的 reducer 大概是長這個樣子 (為了容易理解,在此不使用 ES6 / Immutable.js):
/** 本代碼塊記為 code-7 **/ var initState = {counter: 0,todos: [] }function reducer(state, action) {// ※ 應(yīng)用的初始狀態(tài)是在第一次執(zhí)行 reducer 時設(shè)置的(除非是服務(wù)端渲染) ※if (!state) state = initStateswitch (action.type) {case 'ADD_TODO':var nextState = _.deepClone(state) // 用到了 lodash 的深克隆nextState.todos.push(action.payload) return nextStatedefault:// 由于 nextState 會把原 state 整個替換掉// 若無修改,必須返回原 state(否則就是 undefined)return state} }通俗點講,就是 reducer 返回啥,state 就被替換成啥
§ 總結(jié)
store 由 Redux 的 createStore(reducer) 生成
state 通過 store.getState() 獲取,本質(zhì)上一般是一個存儲著整個應(yīng)用狀態(tài)的對象
action 本質(zhì)上是一個包含 type 屬性的普通對象,由 Action Creator (函數(shù)) 產(chǎn)生
改變 state 必須 dispatch 一個 action
reducer 本質(zhì)上是根據(jù) action.type 來更新 state 并返回 nextState 的函數(shù)
reducer 必須返回值,否則 nextState 即為 undefined
實際上,state 就是所有 reducer 返回值的匯總(本教程只有一個 reducer,主要是應(yīng)用場景比較簡單)
Action Creator => action => store.dispatch(action) => reducer(state, action) => 原 state state = nextState
⊙ Redux 與傳統(tǒng)后端 MVC 的對照
| store | 數(shù)據(jù)庫實例 |
| state | 數(shù)據(jù)庫中存儲的數(shù)據(jù) |
| dispatch(action) | 用戶發(fā)起請求 |
| action: { type, payload } | type 表示請求的 URL,payload 表示請求的數(shù)據(jù) |
| reducer | 路由 + 控制器(handler) |
| reducer 中的 switch-case 分支 | 路由,根據(jù) action.type 路由到對應(yīng)的控制器 |
| reducer 內(nèi)部對 state 的處理 | 控制器對數(shù)據(jù)庫進(jìn)行增刪改操作 |
| reducer 返回 nextState | 將修改后的記錄寫回數(shù)據(jù)庫 |
§ 最簡單的例子 ( 在線演示 )
<!DOCTYPE html> <html> <head><script src="//cdn.bootcss.com/redux/3.5.2/redux.min.js"></script> </head> <body> <script> /** Action Creators */ function inc() {return { type: 'INCREMENT' }; } function dec() {return { type: 'DECREMENT' }; }function reducer(state, action) {// 首次調(diào)用本函數(shù)時設(shè)置初始 statestate = state || { counter: 0 };switch (action.type) {case 'INCREMENT':return { counter: state.counter + 1 };case 'DECREMENT':return { counter: state.counter - 1 };default:return state; // 無論如何都返回一個 state} }var store = Redux.createStore(reducer);console.log( store.getState() ); // { counter: 0 }store.dispatch(inc()); console.log( store.getState() ); // { counter: 1 }store.dispatch(inc()); console.log( store.getState() ); // { counter: 2 }store.dispatch(dec()); console.log( store.getState() ); // { counter: 1 } </script> </body> </html>由上可知,Redux 并不一定要搭配 React 使用。Redux 純粹只是一個狀態(tài)管理庫,幾乎可以搭配任何框架使用
(上述例子連 jQuery 都沒用哦親)
§ 下一章:Redux 進(jìn)階教程
總結(jié)
以上是生活随笔為你收集整理的Redux 莞式教程 之 简明篇的全部內(nèi)容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 快速学习Android开发知识点总结(磨
- 下一篇: spring配置的相关文章