原文連接(保持更新):https://github.com/kenberkele...css
寫在前面
拋開需求講實用性都是耍流氓,所以下面由我扮演您那可親可愛的產品經理react
不知道您是否有後端的開發經驗,後端通常會有記錄訪問日誌的中間件
例如,在 Express 中實現一個簡單的 Logger 以下:git
var loggerMiddleware = function(req, res, next) { console.log('[Logger]', req.method, req.originalUrl) next() } ... app.use(loggerMiddleware)
每次訪問的時候,都會在控制檯中留下相似下面的日誌便於追蹤調試:程序員
[Logger] GET / [Logger] POST /login [Logger] GET /user?uid=10086 ...
若是咱們把場景轉移到前端,請問該如何實現用戶的動做跟蹤記錄?
咱們可能會這樣寫:github
/** jQuery **/ $('#loginBtn').on('click', function(e) { console.log('[Logger] 用戶登陸') ... }) $('#logoutBtn').on('click', function() { console.log('[Logger] 用戶退出登陸') ... }) /** MVC / MVVM 框架(這裏以純 Vue 舉例) **/ methods: { handleLogin () { console.log('[Logger] 用戶登陸') ... }, handleLogout () { console.log('[Logger] 用戶退出登陸') ... } }
上述 jQuery 與 MV* 的寫法並無本質上的區別
記錄用戶行爲代碼的侵入性極強,可維護性與擴展性堪憂數據庫
哼!最討厭就是改需求了,這種簡單的需求難道不是應該一開始就想好的嗎?
呵呵,若是每位產品經理都能一開始就把需求完善好,咱們就不用加班了好伐redux
顯然地,前端的童鞋又得一個一個去改(固然 編輯器 / 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)
難道您覺得有了 UglifyJS,配置一個 drop_console: true
就行了嗎?圖樣圖森破,拿衣服!
請看清楚了,僅僅是去掉有關 Logger 的 console.log
,其餘的要保留哦親~~~
因而前端的童鞋又不得不乖乖地一個一個註釋掉(固然也能夠設置一個環境變量判斷是否輸出,甚至能夠重寫 console.log
)
而咱們後端的童鞋呢?只須要註釋掉一行代碼便可:// app.use(loggerMiddleware)
,真可謂是不費吹灰之力
收集用戶報錯仍是比較簡單的,利用 window.error
事件,而後根據 Source Map 定位到源碼(但通常查不出什麼)
但要徹底還原出當時的使用場景,幾乎是不可能的。由於您不知道這個報錯,用戶是怎麼一步一步操做得來的
就算知道用戶是如何操做得來的,但在您的電腦上,測試永遠都是經過的(不是我寫的程序有問題,是用戶用的方式有問題)
相對地,後端的報錯的收集、定位以及還原倒是至關簡單。只要一個 API 有 bug,那不管用什麼設備訪問,都會獲得這個 bug
還原 bug 也是至關簡單:把數據庫備份導入到另外一臺機器,部署一樣的運行環境與代碼。如無心外,bug 確定能夠完美重現
在這個問題上拿後端跟前端對比,確實有失公允。但爲了鼓吹 Redux 的優越,只能勉爲其難了
實際上 jQuery / MV* 中也能實現用戶動做的跟蹤,用一個數組往裏面
push
用戶動做便可
但這樣操做的意義不大,由於僅僅只有動做,沒法反映動做先後,應用狀態的變更狀況
爲什麼先後端對於這類需求的處理居然截然不同?後端爲什麼能夠如此優雅?
緣由在於,後端具備統一的入口與統一的狀態管理(數據庫),所以能夠引入中間件機制來統一實現某些功能
多年來,前端工程師忍辱負重,操着賣白粉的心,賺着買白菜的錢,一直處於程序員鄙視鏈的底層
因而有大牛就把後端 MVC 的開發思惟搬到前端,將應用中全部的動做與狀態都統一管理,讓一切有據可循
使用 Redux,藉助 Redux DevTools 能夠實現出「華麗如時光旅行通常的調試效果」
實際上就是開發調試過程當中能夠撤銷與重作,而且支持應用狀態的導入和導出(就像是數據庫的備份)
並且,因爲可使用日誌完整記錄下每一個動做,所以作到像 Git 般,隨時隨地恢復到以前的狀態
因爲能夠導出和導入應用的狀態(包括路由狀態),所以還能夠實現先後端同構(服務端渲染)
固然,既然有了動做日誌以及動做先後的狀態備份,那麼還原用戶報錯場景還會是一個難題嗎?
首先要區分 store
和 state
state
是應用的狀態,通常本質上是一個普通對象
例如,咱們有一個 Web APP,包含 計數器 和 待辦事項 兩大功能
那麼咱們能夠爲該應用設計出對應的存儲數據結構(應用初始狀態):
/** 應用初始 state,本代碼塊記爲 code-1 **/ { counter: 0, todos: [] }
store
是應用狀態 state
的管理者,包含下列四個函數:
getState() # 獲取整個 state
dispatch(action) # ※ 觸發 state 改變的【惟一途徑】※
subscribe(listener) # 您能夠理解成是 DOM 中的 addEventListener
replaceReducer(nextReducer) # 通常在 Webpack Code-Splitting 按需加載的時候用
兩者的關係是:state = store.getState()
Redux 規定,一個應用只應有一個單一的 store
,其管理着惟一的應用狀態 state
Redux 還規定,不能直接修改應用的狀態 state
,也就是說,下面的行爲是不容許的:
var state = store.getState() state.counter = state.counter + 1 // 禁止在業務邏輯中直接修改 state
若要改變 state
,必須 dispatch
一個 action
,這是修改應用狀態的不二法門
如今您只須要記住
action
只是一個包含type
屬性的普通對象便可
例如{ type: 'INCREMENT' }
上面提到,state
是經過 store.getState()
獲取,那麼 store
又是怎麼來的呢?
想生成一個 store
,咱們須要調用 Redux 的 createStore
:
import { createStore } from 'redux' ... const store = createStore(reducer, initialState) // store 是靠傳入 reducer 生成的哦!
如今您只須要記住
reducer
是一個 函數,負責更新並返回一個新的state
而initialState
主要用於先後端同構的數據同步(詳情請關注 React 服務端渲染)
上面提到,action
(動做)實質上是包含 type
屬性的普通對象,這個 type
是咱們實現用戶行爲追蹤的關鍵
例如,增長一個待辦事項 的 action
多是像下面同樣:
/** 本代碼塊記爲 code-2 **/ { type: 'ADD_TODO', payload: { id: 1, content: '待辦事項1', completed: false } }
固然,action
的形式是多種多樣的,惟一的約束僅僅就是包含一個 type
屬性罷了
也就是說,下面這些 action
都是合法的:
/** 以下都是合法的,但就是不夠規範 **/ { type: 'ADD_TODO', id: 1, content: '待辦事項1', completed: false } { type: 'ADD_TODO', abcdefg: { id: 1, content: '待辦事項1', completed: false } }
雖然說沒有約束,但最好仍是遵循規範
若是須要新增一個代辦事項,實際上就是將 code-2
中的 payload
「寫入」 到 state.todos
數組中(如何「寫入」?在此留個懸念):
/** 本代碼塊記爲 code-3 **/ { counter: 0, todos: [{ id: 1, content: '待辦事項1', completed: false }] }
刨根問底,action
是誰生成的呢?
Action Creator 能夠是同步的,也能夠是異步的
顧名思義,Action Creator 是 action
的創造者,本質上就是一個函數,返回值是一個 action
(對象)
例以下面就是一個 「新增一個待辦事項」 的 Action Creator:
/** 本代碼塊記爲 code-4 **/ var id = 1 function addTodo(content) { return { type: 'ADD_TODO', payload: { id: id++, content: content, // 待辦事項內容 completed: false // 是否完成的標識 } } }
將該函數應用到一個表單(假設 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) // 執行 Action Creator 得到 action store.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
明明就沒有強制的規範,爲何 store.dispatch(action)
以後,
Redux 會明確知道是提取 action.payload
,而且是對應寫入到 state.todos
數組中?
又是誰負責「寫入」的呢?懸念即將揭曉...
Reducer 必須是同步的純函數
用戶每次 dispatch(action)
後,都會觸發 reducer
的執行 reducer
的實質是一個函數,根據 action.type
來更新 state
並返回 nextState
最後會用 reducer
的返回值 nextState
徹底替換掉原來的 state
注意:上面的這個 「更新」 並非指
reducer
能夠直接對state
進行修改
Redux 規定,須先複製一份state
,在副本nextState
上進行修改操做
例如,可使用 lodash 的deepClone
,也可使用Object.assign / map / filter/ ...
等返回副本的函數
在上面 Action Creator 中提到的 待辦事項的 reducer
大概是長這個樣子 (爲了容易理解,在此不使用 ES6 / Immutable.js):
/** 本代碼塊記爲 code-7 **/ var initState = { counter: 0, todos: [] } function reducer(state, action) { // ※ 應用的初始狀態是在第一次執行 reducer 時設置的(除非是服務端渲染) ※ if (!state) state = initState switch (action.type) { case 'ADD_TODO': var nextState = _.deepClone(state) // 用到了 lodash 的深克隆 nextState.todos.push(action.payload) return nextState default: // 因爲 nextState 會把原 state 整個替換掉 // 若無修改,必須返回原 state(不然就是 undefined) return state } }
通俗點講,就是
reducer
返回啥,state
就被替換成啥
store
由 Redux 的 createStore(reducer)
生成
state
經過 store.getState()
獲取,本質上通常是一個存儲着整個應用狀態的對象
action
本質上是一個包含 type
屬性的普通對象,由 Action Creator (函數) 產生
改變 state
必須 dispatch
一個 action
reducer
本質上是根據 action.type
來更新 state
並返回 nextState
的函數
reducer
必須返回值,不然 nextState
即爲 undefined
實際上,state
就是全部 reducer
返回值的彙總(本教程只有一個 reducer
,主要是應用場景比較簡單)
Action Creator =>
action
=>store.dispatch(action)
=>reducer(state, action)
=>原 state
state = nextState
Redux | 傳統後端 MVC |
---|---|
store |
數據庫實例 |
state |
數據庫中存儲的數據 |
dispatch(action) |
用戶發起請求 |
action: { type, payload } |
type 表示請求的 URL,payload 表示請求的數據 |
reducer |
路由 + 控制器(handler) |
reducer 中的 switch-case 分支 |
路由,根據 action.type 路由到對應的控制器 |
reducer 內部對 state 的處理 |
控制器對數據庫進行增刪改操做 |
reducer 返回 nextState |
將修改後的記錄寫回數據庫 |
<!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) { // 首次調用本函數時設置初始 state state = 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 純粹只是一個狀態管理庫,幾乎能夠搭配任何框架使用
(上述例子連 jQuery 都沒用哦親)