Redux 極簡明教程

Redux 簡明教程

原文鏈接(保持更新):https://github.com/kenberkeley/redux-simple-tutorial

寫在前面

本教程深入淺出能曾,配套 簡明教程、進階教程(源碼精讀)以及文檔注釋豐滿的 Demo 等一條龍服務(wù)

§ 為什么要用 Redux

當然還有 Flux强衡、Reflux倡怎、Mobx 等狀態(tài)管理庫可供選擇

拋開需求講實用性都是耍流氓鳍悠,因此下面由我扮演您那可親可愛的產(chǎn)品經(jīng)理

⊙ 需求 1:在控制臺上記錄用戶的每個動作

不知道您是否有后端的開發(fā)經(jīng)驗沃暗,后端一般會有記錄訪問日志的中間件
例如瞎颗,在 Express 中實現(xiàn)一個簡單的 Logger 如下:

var loggerMiddleware = function(req, res, next) {
  console.log('[Logger]', req.method, req.originalUrl)
  next()
}
...
app.use(loggerMiddleware)

每次訪問的時候件甥,都會在控制臺中留下類似下面的日志便于追蹤調(diào)試:

[Logger] GET  /
[Logger] POST /login
[Logger] GET  /user?uid=10086
...

如果我們把場景轉(zhuǎn)移到前端,請問該如何實現(xiàn)用戶的動作跟蹤記錄哼拔?
我們可能會這樣寫:

/** 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* 的寫法并沒有本質(zhì)上的區(qū)別
記錄用戶行為代碼的侵入性極強引有,可維護性與擴展性堪憂

⊙ 需求 2:在上述需求的基礎(chǔ)上,記錄用戶的操作時間

哼倦逐!最討厭就是改需求了譬正,這種簡單的需求難道不是應(yīng)該一開始就想好的嗎?
呵呵檬姥,如果每位產(chǎn)品經(jī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,其他的要保留哦親~~~
于是前端的童鞋又不得不乖乖地一個一個注釋掉(當然也可以設(shè)置一個環(huán)境變量判斷是否輸出型诚,甚至可以重寫 console.log

而我們后端的童鞋呢客燕?只需要注釋掉一行代碼即可:// app.use(loggerMiddleware),真可謂是不費吹灰之力

⊙ 需求 4:正式上線后俺驶,自動收集 bug幸逆,并還原出當時的場景

收集用戶報錯還是比較簡單的,利用 window.error 事件暮现,然后根據(jù) Source Map 定位到源碼(但一般查不出什么)

但要完全還原出當時的使用場景还绘,幾乎是不可能的。因為您不知道這個報錯栖袋,用戶是怎么一步一步操作得來的
就算知道用戶是如何操作得來的拍顷,但在您的電腦上,測試永遠都是通過的(不是我寫的程序有問題塘幅,是用戶用的方式有問題)

相對地昔案,后端的報錯的收集、定位以及還原卻是相當簡單电媳。只要一個 API 有 bug踏揣,那無論用什么設(shè)備訪問,都會得到這個 bug
還原 bug 也是相當簡單:把數(shù)據(jù)庫備份導入到另一臺機器匾乓,部署同樣的運行環(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ù)庫)犬第,因此可以引入中間件機制統(tǒng)一實現(xiàn)某些功能

多年來,前端工程師忍辱負重芒帕,操著賣白粉的心,賺著買白菜的錢丰介,一直處于程序員鄙視鏈的底層
于是有大牛就把后端 MVC 的開發(fā)思維搬到前端背蟆,將應(yīng)用中所有的動作與狀態(tài)都統(tǒng)一管理鉴分,讓一切有據(jù)可循

使用 Redux,借助 Redux DevTools 可以實現(xiàn)出“華麗如時光旅行一般的調(diào)試效果”
實際上就是開發(fā)調(diào)試過程中可以撤銷與重做带膀,并且支持應(yīng)用狀態(tài)的導入和導出(就像是數(shù)據(jù)庫的備份)
而且余佃,由于可以使用日志完整記錄下每個動作头滔,因此做到像 Git 般,隨時隨地恢復到之前的狀態(tài)

由于可以導出和導入應(yīng)用的狀態(tài)(包括路由狀態(tài)),因此還可以實現(xiàn)前后端同構(gòu)(服務(wù)端渲染)
當然敞咧,既然有了動作日志以及動作前后的狀態(tài)備份,那么還原用戶報錯場景還會是一個難題嗎抄邀?

§ Store

首先要區(qū)分 storestate

state 是應(yīng)用的狀態(tài)掠兄,一般本質(zhì)上是一個普通對象
例如,我們有一個 Web APP剂癌,包含 計數(shù)器 和 待辦事項 兩大功能
那么我們可以為該應(yīng)用設(shè)計出對應(yīng)的存儲數(shù)據(jù)結(jié)構(gòu)(應(yīng)用初始狀態(tài)):

/** 應(yīng)用初始 state淤翔,本代碼塊記為 code-1 **/
{
  counter: 0,
  todos: []
}

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谐檀,也就是說抡谐,下面的行為是不允許的:

var state = store.getState()
state.counter = state.counter + 1 // 禁止在業(yè)務(wù)邏輯中直接修改 state

若要改變 state,必須 dispatch 一個 action桐猬,這是修改應(yīng)用狀態(tài)的不二法門

現(xiàn)在您只需要記住 action 只是一個包含 type 屬性的普通對象即可
例如 { type: 'INCREMENT' }

上面提到麦撵,state 是通過 store.getState() 獲取,那么 store 又是怎么來的呢课幕?
想生成一個 store厦坛,我們需要調(diào)用 Redux 的 createStore

import { createStore } from 'redux'
...
const store = createStore(reducer, initialState) // store 是靠傳入 reducer 生成的哦!

現(xiàn)在您只需要記住 reducer 是一個 函數(shù)乍惊,負責更新并返回一個新的 state
initialState 主要用于前后端同構(gòu)的數(shù)據(jù)同步(詳情請關(guān)注 React 服務(wù)端渲染)

§ Action

上面提到杜秸,action(動作)實質(zhì)上是包含 type 屬性的普通對象,這個 type 是我們實現(xiàn)用戶行為追蹤的關(guān)鍵
例如润绎,增加一個待辦事項 的 action 可能是像下面一樣:

/** 本代碼塊記為 code-2 **/
{
  type: 'ADD_TODO',
  payload: {
    id: 1,
    content: '待辦事項1',
    completed: false
  }
}

當然撬碟,action 的形式是多種多樣的,唯一的約束僅僅就是包含一個 type 屬性罷了
也就是說莉撇,下面這些 action 都是合法的:

/** 如下都是合法的呢蛤,但就是不夠規(guī)范 **/
{
  type: 'ADD_TODO',
  id: 1,
  content: '待辦事項1',
  completed: false
}

{
  type: 'ADD_TODO',
  abcdefg: {
    id: 1,
    content: '待辦事項1',
    completed: false
  }
}

雖說沒有約束,但最好還是遵循規(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:

/** 本代碼塊記為 code-4 **/
var id = 1
function addTodo(content) {
  return {
    type: 'ADD_TODO',
    payload: {
      id: id++,
      content: content, // 待辦事項內(nèi)容
      completed: false  // 是否完成的標識
    }
  }
}

將該函數(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 獲得 action
  store.dispatch(action) // 改變 state 的不二法門:dispatch 一個 action!T焖浮否过!
})
</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 明明就沒有強制的規(guī)范誉察,為什么 store.dispatch(action) 之后,
Redux 會明確知道是提取 action.payload惹谐,并且是對應(yīng)寫入到 state.todos 數(shù)組中持偏?
又是誰負責“寫入”的呢?懸念即將揭曉...

§ Reducer

Reducer 必須是同步的純函數(shù)

用戶每次 dispatch(action) 后氨肌,都會觸發(fā) reducer 的執(zhí)行
reducer 的實質(zhì)是一個函數(shù)鸿秆,根據(jù) action.type更新 state 并返回 nextState
最后會用 reducer 的返回值 nextState 完全替換掉原來的 state

注意:上面的這個 “更新” 并不是指 reducer 可以直接對 state 進行修改
Redux 規(guī)定,須先復制一份 state怎囚,在副本 nextState 上進行修改操作
例如卿叽,可以使用 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 = 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 就被替換成啥

§ 總結(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 的對照

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ù)庫進行增刪改操作
reducer 返回 nextState 將修改后的記錄寫回數(shù)據(jù)庫

§ 最簡單的例子 ( 在線演示 )

<!DOCTYPE html>
<html>
<head>
  <script src="http://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è)置初始 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 純粹只是一個狀態(tài)管理庫枕稀,幾乎可以搭配任何框架使用
(上述例子連 jQuery 都沒用哦親)

§ 下一章:Redux 進階教程

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現(xiàn)的幾起案子抽莱,更是在濱河造成了極大的恐慌范抓,老刑警劉巖,帶你破解...
    沈念sama閱讀 211,817評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件食铐,死亡現(xiàn)場離奇詭異,居然都是意外死亡僧鲁,警方通過查閱死者的電腦和手機虐呻,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,329評論 3 385
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來寞秃,“玉大人斟叼,你說我怎么就攤上這事〈菏伲” “怎么了朗涩?”我有些...
    開封第一講書人閱讀 157,354評論 0 348
  • 文/不壞的土叔 我叫張陵,是天一觀的道長绑改。 經(jīng)常有香客問我谢床,道長,這世上最難降的妖魔是什么厘线? 我笑而不...
    開封第一講書人閱讀 56,498評論 1 284
  • 正文 為了忘掉前任识腿,我火速辦了婚禮,結(jié)果婚禮上造壮,老公的妹妹穿的比我還像新娘渡讼。我一直安慰自己,他們只是感情好耳璧,可當我...
    茶點故事閱讀 65,600評論 6 386
  • 文/花漫 我一把揭開白布成箫。 她就那樣靜靜地躺著,像睡著了一般旨枯。 火紅的嫁衣襯著肌膚如雪蹬昌。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,829評論 1 290
  • 那天召廷,我揣著相機與錄音凳厢,去河邊找鬼。 笑死竞慢,一個胖子當著我的面吹牛先紫,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播筹煮,決...
    沈念sama閱讀 38,979評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼遮精,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起本冲,我...
    開封第一講書人閱讀 37,722評論 0 266
  • 序言:老撾萬榮一對情侶失蹤准脂,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后檬洞,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體狸膏,經(jīng)...
    沈念sama閱讀 44,189評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,519評論 2 327
  • 正文 我和宋清朗相戀三年添怔,在試婚紗的時候發(fā)現(xiàn)自己被綠了湾戳。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,654評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡广料,死狀恐怖砾脑,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情艾杏,我是刑警寧澤韧衣,帶...
    沈念sama閱讀 34,329評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站购桑,受9級特大地震影響畅铭,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜其兴,卻給世界環(huán)境...
    茶點故事閱讀 39,940評論 3 313
  • 文/蒙蒙 一顶瞒、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧元旬,春花似錦榴徐、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,762評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至穆端,卻和暖如春袱贮,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背体啰。 一陣腳步聲響...
    開封第一講書人閱讀 31,993評論 1 266
  • 我被黑心中介騙來泰國打工攒巍, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人荒勇。 一個月前我還...
    沈念sama閱讀 46,382評論 2 360
  • 正文 我出身青樓柒莉,卻偏偏與公主長得像,于是被迫代替她去往敵國和親沽翔。 傳聞我的和親對象是個殘疾皇子兢孝,可洞房花燭夜當晚...
    茶點故事閱讀 43,543評論 2 349

推薦閱讀更多精彩內(nèi)容