Redux 莞式教程 之 簡明篇

Redux 簡明教程

原文連接(保持更新):https://github.com/kenberkele...css

寫在前面

本教程深刻淺出,配套 簡明教程、進階教程(源碼精讀)以及文檔註釋豐滿的 Demo 等一條龍服務html

§ 爲何要用 Redux

固然還有 FluxRefluxMobx 等狀態管理庫可供選擇前端

拋開需求講實用性都是耍流氓,所以下面由我扮演您那可親可愛的產品經理react

⊙ 需求 1:在控制檯上記錄用戶的每一個動做

不知道您是否有後端的開發經驗,後端通常會有記錄訪問日誌的中間件
例如,在 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* 的寫法並無本質上的區別
記錄用戶行爲代碼的侵入性極強,可維護性與擴展性堪憂數據庫

⊙ 需求 2:在上述需求的基礎上,記錄用戶的操做時間

哼!最討厭就是改需求了,這種簡單的需求難道不是應該一開始就想好的嗎?
呵呵,若是每位產品經理都能一開始就把需求完善好,咱們就不用加班了好伐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)

⊙ 需求 3:正式上線的時候,把控制檯中有關 Logger 的輸出所有去掉

難道您覺得有了 UglifyJS,配置一個 drop_console: true 就行了嗎?圖樣圖森破,拿衣服!
請看清楚了,僅僅是去掉有關 Logger 的 console.log,其餘的要保留哦親~~~
因而前端的童鞋又不得不乖乖地一個一個註釋掉(固然也能夠設置一個環境變量判斷是否輸出,甚至能夠重寫 console.log

而咱們後端的童鞋呢?只須要註釋掉一行代碼便可:// app.use(loggerMiddleware),真可謂是不費吹灰之力

⊙ 需求 4:正式上線後,自動收集 bug,並還原出當時的場景

收集用戶報錯仍是比較簡單的,利用 window.error 事件,而後根據 Source Map 定位到源碼(但通常查不出什麼)

但要徹底還原出當時的使用場景,幾乎是不可能的。由於您不知道這個報錯,用戶是怎麼一步一步操做得來的
就算知道用戶是如何操做得來的,但在您的電腦上,測試永遠都是經過的(不是我寫的程序有問題,是用戶用的方式有問題)

相對地,後端的報錯的收集、定位以及還原倒是至關簡單。只要一個 API 有 bug,那不管用什麼設備訪問,都會獲得這個 bug
還原 bug 也是至關簡單:把數據庫備份導入到另外一臺機器,部署一樣的運行環境與代碼。如無心外,bug 確定能夠完美重現

在這個問題上拿後端跟前端對比,確實有失公允。但爲了鼓吹 Redux 的優越,只能勉爲其難了

實際上 jQuery / MV* 中也能實現用戶動做的跟蹤,用一個數組往裏面 push 用戶動做便可
但這樣操做的意義不大,由於僅僅只有動做,沒法反映動做先後,應用狀態的變更狀況

※ 小結

爲什麼先後端對於這類需求的處理居然截然不同?後端爲什麼能夠如此優雅?
緣由在於,後端具備統一的入口統一的狀態管理(數據庫),所以能夠引入中間件機制統一實現某些功能

多年來,前端工程師忍辱負重,操着賣白粉的心,賺着買白菜的錢,一直處於程序員鄙視鏈的底層
因而有大牛就把後端 MVC 的開發思惟搬到前端,將應用中全部的動做與狀態都統一管理,讓一切有據可循

使用 Redux,藉助 Redux DevTools 能夠實現出「華麗如時光旅行通常的調試效果」
實際上就是開發調試過程當中能夠撤銷與重作,而且支持應用狀態的導入和導出(就像是數據庫的備份)
並且,因爲可使用日誌完整記錄下每一個動做,所以作到像 Git 般,隨時隨地恢復到以前的狀態

因爲能夠導出和導入應用的狀態(包括路由狀態),所以還能夠實現先後端同構(服務端渲染)
固然,既然有了動做日誌以及動做先後的狀態備份,那麼還原用戶報錯場景還會是一個難題嗎?

§ Store

首先要區分 storestate

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

上面提到,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 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

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 的對照

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 都沒用哦親)

§ 下一章:Redux 進階教程

相關文章
相關標籤/搜索