翻譯| 10 Tips for Better Redux Architecture

Redux對于React程序是可有可無的嗎?當你認識到Redux在編程時給你那種可以掌控一切狀態能力的時候,你會覺得如果沒有這種思考方法,我們到底該怎么來實現其中的邏輯呢?React中對于組件的控制引入了兩個東西一個是props,一個是state.如果站在單個組件的角度上,組件本身其實也是一個復雜的復合體,給我們只開放兩個能決定組件可以做什么,可以怎么表現的接口,就是這兩個東西. 哲學說(呵呵,不是我說的,后面文章里的東西):一切事物皆由外因.外因的來源其實也有很多種組織方法.我曾經踢過很長時間的足球,只是踢踢野球罷了,踢野球可是沒有任何章法可言,對陣雙方可能是雜合著各種踢法和戰術,或者是沒有戰術,好像也是在踢足球,但是大家都是憑著愛好和從觀看比賽時積累的一點不成熟的想法.但是和有專門戰術訓練,有固定的戰術打法的職業隊一比沒有任何的戰斗能力.即便大家看不上的中國足球,隨便拿一只大學校隊級別的球隊和野球隊比賽,野球隊都差的十萬八千里.這里不談個人的技術素養.在場地上的球員都是按照既定的方案在運行,也即是其實受到教練和戰術訓練的控制的.這一點非常的高效啊.我都不知道我怎么把React/Redux的思想和足球的思想聯系了起來.但是好像能說明一點問題. 還有就是要使用新思想,腦袋要給新思想完全的空間. 中國足球隊現在請了世界頂級的教練,成績依然不是太好,既然請了人家的教練,就完全的配合,不要用什么中國國情特殊來對抗教練的思想.這對于Redux在React中的實施也是很重要的.要用Redux就要完全接受這套思想,不要老想著這怎么和原來的不一樣啊!不能這么搞! 重要的還是思想. Facebook在實現React已經給state的管理提供了一個很好的選型構架,Redux只不過這比較好的實現了這個構架.別猶豫了,要使用React,徹底的投入Redux的懷抱吧!**再啰嗦總結一下:Redux通過props完全掌控了React組件的一舉一動,我們可以在組件外觀察組件能做什么,會發生什么變化 **

下面的這篇文章在meidum中收到了1000過個贊,的確是值得推薦.其實根本的問題Redux文檔中的三個原則已經總結的太好了.但是思想的轉變真的不是一朝一夕的功夫!需要花更多的時間給你的大腦回路形成的時間.一旦你的大腦再看到Redux的時候,有一個神經元激活了,那就基本成功了(??,題外話,喜歡美劇老友記的可以搜搜鏡像神經元和珍妮弗.安妮斯頓.非常有意思的研究).

原文請見

以下為譯文內容


當我開始使用React的時候,Redux還沒出生呢?僅僅只有Flux構架以及一堆實現這個構架的方案.

現在在React的數據管理方面有兩個明顯的勝利者:Redux和MobX,MobX甚至都沒有使用Flux架構.Redux之所以這么吸引眼球,原因他不在僅僅是為React服務了.你可以發現Redux在其他的框架上也已經有實施方案.包括Angular2,例如ngrx:store.

note1 MobX非???在簡單的UI中我可能會使用它而不是Redux,MobX更簡單,也不太啰嗦(譯注:的確是,你想要既簡單又高效的東西那不可能.這種事情是不會發生的).這也就是說Redux有一些特性是MobX不能給你的.在項目中是否使用Redux之前理解這些Redux的獨有特性是非常重要的.
note2 Relay和falcor是另外兩個狀態管理的解決方案.但是他們分別要由GraphQL和Falcor server來提供后臺支持.Relay的state都和服務器端的持久化數據對應著.AFAIK(目前我知道的是),兩者都不能提供客戶端暫時的
state管理方案.你可能會很喜歡結合Relay或者Falcor和Redu或MobX,結合使用其中在服務端和客戶端state管理的能力.底線:在客戶端的state管理上沒有明顯的勝利者.使用手頭可以用的最好工具.

Redux的創建者Dan Abramov有一系列關于這個主題的課程:

兩者都是循序漸進的介紹Redux的基礎.但是你會需要對Redux的理解提升到更高的水平.

下面的這些小Tips幫助你構建更好的Redux app

1. Understanding the Benefits of Redux

Redux有兩個至關重要的目標,你要牢記于心:

  1. View 渲染的確定性(Deterministic)
  2. State變化的確定性(Deterministic)

確定性對于應用的測試,診斷和修復bugs來說都是非常重要的.如果你的應用視圖和狀態是不確定的(nondeterministic)的,根本就不可能知道視圖和狀態是不是有效. 或許nonndeterministic本身就是一個bug.

但是有些事情內在就是nodedeterministic的,例如用戶輸入和網絡的I/O操作.我們怎么知道代碼是否正常工作呢? 簡單:隔離.(譯注:為什么說好測試呢?舉個例子:
你要是血糖高,流動在血管里的血是沒有辦法測糖含量的,我們把它抽出來,才能用試紙來測量.這就是所謂的隔離或者分離啊)

Redux的主要目的就是要從I/O端的異步操作例如視圖的渲染和網絡的工作中把state management隔離出來.當異步操作隔離出來以后,代碼就變得非常的簡單,立即和測試業務代碼也非常的簡單了,因為這些代碼不再和網絡請求以及DOM操作糾纏在一起.

當你的視圖渲染從網絡I/O和狀態更新隔離開來以后,你就會得到一個外因決定的 視圖渲染方法,意思是:只要給定相同的state,視圖一定會渲染出相同的結果.這么做消除了諸如異步操作中競爭條件對于視圖的影響以及視圖在渲染過程中對state的不完整的截取問題.

當一個新手在思考創建一個視圖的時候,他可能會想:這里需要一個用戶模型,所以我啟動一個異步請求,當promises對象的狀態變為resloves的時候,我就使用用戶的名字來更新用戶組件.這么做比todo中的 items需要的任務多一寫,我們使用fetch模塊來完成他,當promise對象resolves的時候,我們可以遍歷他們,添加到屏幕上.

使用這種方法有幾個主要的問題:

  1. 在任何時間點,你都沒有所有需要渲染視圖的數據.直到組件開始加載之前,你實際都不能開始請求數據.
  2. 不同的遠程請求任務會在不同時間點到來,這會對視圖渲染的隊列有些微妙的變化.為了明白渲染隊列,你需要一些你不可能預料的知識:每一個異步請求的持續時間.小測試:在上面的場景里,那個視圖最先渲染?用戶組件還是todo-items?答案是:這是競爭關系.
  3. 有時候事件監聽器也會更新視圖的狀態,可能還會觸發另一個渲染,更復雜的隊列.

關鍵的問題是:在視圖的狀態里存儲數據,在視圖中添加事件監聽器導致了視圖狀態的突變:

Nondeterminism=并發的處理過程+共享的狀態.-Martin Odersky(Scalas設計者)

數據遠程獲取,數據操作,視圖渲染混合在一起構成了時間旅行式的意大利面條代碼

我知道這里所說的好像是B級科幻電影的套路,但是請相信我,時間旅行式的意大利面是最壞的菜譜!

flux構架強調嚴格的隔離和序列,每一次處理都會遵守這些規則.

  1. 首先,我們會知道,固定的state...
  2. 當我們在渲染視圖的時候,沒有任何事情可以改變state.
  3. 給定同樣的state,渲染的視圖總是相同.
  4. 事件監聽器監聽用戶的輸入和網絡請求句柄.當這些請求有了結果以后,actions會被發送到store.
  5. 當一個action被派發,state被更新到一個新的已知的state,隊列重復.可以改變state的只有派發actions.

Flux構架是一個果殼,單向的數據流動構架.

Flux Arctiture
Flux Arctiture

在Flux構架中,視圖監聽用戶的輸入,把這些輸入翻譯為action對象,action對象可以被dispatch到Store.Store更新應用的state,并且告知視圖再次渲染.當然了,視圖也可以只依賴輸入和事件,這也沒有問題.另外,事件監聽器可以像下面這樣派發action對象:

重要的是,Flux中的state更新是事務性的.代替在state上簡單的調用更新方法,或者支架操作一個值,action會被派發到store.一個Action對象會被事務性的記錄下來.你可以認為這有點像銀行的事務操作-變化記錄的生成.
當你在銀行存一點錢,你賬戶五分鐘前的信息不回被刪除.新的賬戶信息會被添加到交易的歷史記錄中. Action對象在你的應用state中添加一個事務歷史記錄.

Action對象是這個樣子的:

{
 type: ADD_TODO,
 payload: 'Learn Redux'
}

action對象給你的能力是保持所有的state變化的日志資料.這個日志可以根據外部條件重復生成,意思是:

給定相同的初始化state和相同的事務處理,在相同的操作中,你可以獲得相同的state.

這一點的潛意義是:

  1. 容易測試
  2. undo/redo很容易實施
  3. 時間旅行deguging
  4. 持久性-即使state被擦寫掉了,如果你有每個事務的處理記錄,可以很容易的重復他.

試問,誰不想掌控時空的變化?事務性的狀態給你了時間旅行的超能力.

Redux devtoos with slide review
Redux devtoos with slide review

2. 某些Apps不需要Redux

如果你的UI的工作流很簡單,Redux所做的就有點大材小用了.如果你在做一個tic-tac-toe的游戲,你真的需要undo/redo?這些小游戲很少能玩超過一分鐘.如果玩家失敗了,你需要做的只是重置游戲,再次開始就行了.

如果:

  • 用戶流程很簡單
  • 用戶之間沒有交互
  • 你不需要管理服務端事件(SSE)或者websockets
  • 每個視圖從單一數據源獲取數據.

這可能是因為你的app 事件流程太簡單,不值得在state的事務上花費額外的精力.
或許你不需要在app中使用Fluxify化.還有一個簡單的解決方案.看看MobX (譯注:我很慶幸沒有在Redux學習遇到難點的時候,退縮到安全地帶,有的朋友遇到Redux學不下去的時候就退到了MobX了,在我看來Redux是職業足球隊的踢法,MobX是業余球隊踢法).

然而,隨著你的app變得復雜,視圖的復雜性和狀態管理性都增加了,事務性狀態價值增加,MobX不能提供狀態的事務性管理方法.

如果:

  • 用戶的流程很復雜
  • 你的app有很多的用戶工作流
  • 用戶有很多交互聯系
  • 正在使用web sockets或者SSE
  • 從不同的數據來源獲取數據構建單一的視圖(譯注:這個能力是React比MVC框架更高級的地方,頁面中視圖中的不同組件可以各自獲取自己的數據,互相不受干擾)

如果能從事務模型中獲益.Redux對你就非常的合適.

那么關于web sockets和SSE? 當你添加更多的異步I/O來源時,理解app內部的狀態管理變得非常的困難.受外部控制的state和state的事務性處理能簡化理解過程(譯注:redux-logger打印出state的結構的時候,我如釋重負).

我的觀點是:大多數的SaaS產品包含了最新的復雜UI工作流,應該使用事務性state管理.小型的工具app或者簡單原型的app不應該用.用對工具是非常重要的.

3.理解Reducers

Redux=Flux+Functional Programming

Flux使用action對象描述了單向的數據流和事務狀管理,但是對于怎么操作action對象,什么也沒有講.這就是Redux的切入點.

構建Redux state管理的首要模塊就是reducer函數.那么,什么是reducer 函數?

在函數式編程中,普通的工具reducer()或者fold()用來對values列表中的每一個value執行reducer函數,累加單個輸出的value.這里有一個對于JavaScript Array.prototype.reduce()原型的總結.

//這是從codepen拿出來的代碼,瀏覽器console可以看到結果
 const initialState=0;
 const reducer=(state= initialState,data)=>state+data;
 const total=[0,1,2,3].reduce(reducer);
 console.log(total);

在Redux中使用reducers,不是對數組進行操作,而是對系列的action對象應用reducer.記住,action對象是這個樣子的:

 {
 type: ADD_TODO,
 payload: 'Learn Redux'
 }

讓我們從reducer的總結轉到Redux-style的reducer:

 const defaultState = 0;
const reducer = (state = defaultState, action) => {
 switch (action.type) {
   case 'ADD': return state + action.payload;
   default: return state;
 }
};

現在我們可以應用一下測試actions

 const actions = [
 { type: 'ADD', payload: 0 },
 { type: 'ADD', payload: 1 },
 { type: 'ADD', payload: 2 }
];

const total = actions.reduce(reducer, 0); // 3

4.Reducers必須是純函數

為了獲得受控state可重復性,reducers必須是純函數.沒有意外情況,一個純函數:

  1. 給定一個相同的輸入,總是返回相同的輸出值
  2. 沒有異步操作

在Javascript中非常重要的一點,傳遞進函數的所有的非原始對象都是傳引用賦值(references).換句話說,如果你傳遞一個對象到函數,這個函數對對象的屬性做出了改變, 函數外部的對象也跟著發生變化.這就是副作用(side effect).
如果不知道傳遞對象的整個歷史,你不可能知道函數調用的完整意義.這一點很不利于開發.

Reducers應該返回一個新的對象.例如你可以這樣做OBject.assign({},state,{thingToChange}).

數組的參數也是引用賦值的,不能再使用.push()方法把新的items添加到一個數組.因為.push()會突變一個操作,.pop(), .shift(), .unshift(), .reverse(), .splice() 這些方法都不行.
(譯注:突變的意思是對源頭都改變了,突變以后,源頭是什么樣子就沒有辦法看到了)

如果你想在使用數組的時候,安全一點,可以使用 concat()來代替.push(). (譯注:在reducer中使用concat可以參見iReading app)

看看chat Reducer中的ADD_CHAT的例子:

 const ADD_CHAT = 'CHAT::ADD_CHAT';

const defaultState = {
 chatLog: [],
 currentChat: {
   id: 0,
   msg: '',
   user: 'Anonymous',
   timeStamp: 1472322852680
 }
};

const chatReducer = (state = defaultState, action = {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });
   default: return state;
 }
};

如你所見,新的對象使用Object.assign()來創建,添加項目到數組用concat()代替.push()方法.

就我個人來說,我不喜歡去擔心我的State的突變事故,所以之后我會和Redux一起使用immubable data APIs.如果我的state是immutable對象,根本就不用看代碼,就知道對象是不會發生突變事故的.之所以得出這個結論是因為和一個小組工作之后,發現了事故性突變的bugs.
(譯注:immutable.js的運行原理,或者state的原理,其實參考版本庫的管理方法,每次的修改都會有一個唯一的標示來記錄增刪改查的內容)

還有很多的純函數.如果你在app編程中使用Redux,你需要很好的掌握純函數,其他事情你需要放在心上(例如:處理時間,日志和隨機數).
了解更多內容請看Master the Javascript Interview:What is Pure Function.

5. 記住:Reducers一定是所有事實的唯一來源

在你的app中,所有的state應該只有唯一的真相來源,意思是說:state存儲只存儲在一個地方,其他任何需要state的地方,都需要獲取state的引用賦值.

不同的事情有不同的來源也可以.例如,URL可以是用戶請求路徑和請求路徑的唯一來源.或許你的app有一個配置服務,有API URLs的所有內容.這也可以,但是...
當你在Redux store中存儲state時,接入到state,都需要通過Redux.不遵循這個原則可能會導致臟數據或者某種共享式的state 突變bug.

換句話,如果沒有單一來源的原則,你可能會丟失:

  • 受控的視圖渲染
  • 受控的state重現
  • 簡單易行的undo/redo
  • 時間旅行degugging
  • 容易實施的測試

要么用Redux管理的store,要么不用.如果有的地方用,有的地方不用,可能就會抵消使用Redux的好處.

6.為Action Types使用常量

我習慣于確保當你查看action的歷史時,很容易追蹤到使用他們的reducer.如果你的actionsde名字比較短的普通名字例如CHANGE_MESSAGE,會變得很難理解他在app中做什么.如果你的action types有更多描述性的名字例如:CHAT::CHANGE_MESSAGE,顯然很清楚要做什么.

如果你做了一個錯誤的輸入,派發了一個沒有定義的action 常量,app將會拋出一個異常警告你錯誤.如果會你輸錯了action的累心字符串,action將不會顯示報錯信息而失敗.

把所有的action type收集在一個文件的頂端可以幫助你:

  • 保持名字的統一
  • 快速理解reducer的API
  • 理解請求中的變化

7.使用Action Creators從派發調用中解耦Action的邏輯

當我告訴其他人他們沒有生成IDS或者在reducer中獲取當前時間,我看到的是滑稽的表情.如果你現在盯著屏幕感到疑惑:你也不是唯一這么想的.

有沒有一個好的地方來處理純邏輯,不要在需要使用action的地方重復他們?有,請使用action creator.

Action creators有其他的好處:

  • 把action type常量封裝在reducer文件中,不能在其他地方導入.
  • 在派發action之前對輸入做一些計算
  • Reduce模板

讓我們來使用一個action creator 生成一個ADD_CHATaction 對象:

 // Action creators can be impure.
export const addChat = ({
// cuid is safer than random uuids/v4 GUIDs
// see usecuid.org
id = cuid(),
msg = '',
user = 'Anonymous',
timeStamp = Date.now()
} = {}) => ({
type: ADD_CHAT,
payload: { id, msg, user, timeStamp }
});

如你所見,我們使用cuid為每個聊天信息生成隨機的ids,使用Date.Now()生成時間戳.兩者都是純操作,在reducer中運行不太安全.但是在action creatros中運行時可以的.

使用Action Creators來減少模板代碼使用

一些程序員認為是使用action creators添加模板到項目中.相反的,你將會看到我怎么用他們來大幅度在reducer中減少模板.

提示: 如果你把常量,reducer和action creators放到一個文件中,當你需要從不同的路徑導入他們的時候,你就可以減少模板的需求.

想象著,我們需要給聊天用戶添加定制他們的用戶姓名和可用狀態的能力.我們可能會像下面一樣天界一列的action type:

 const chatReducer = (state = defaultState, action = {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });
   case CHANGE_STATUS:
     return Object.assign({}, state, {
       statusMessage: payload
     });
   case CHANGE_USERNAME:
     return Object.assign({}, state, {
       userName: payload
     });
   default: return state;
 }
};

對于大多數的reducers,這可能會增加一些模板代碼.很多我需要構建的reducer比這個更復雜,他們有一些冗余的代碼.如果我要把這些簡單的屬性改變action融合到一起怎么樣?

事實是,很容易:

  const chatReducer = (state = defaultState, action =    {}) => {
 const { type, payload } = action;
 switch (type) {
   case ADD_CHAT:
     return Object.assign({}, state, {
       chatLog: state.chatLog.concat(payload)
     });

    // Catch all simple changes
    case CHANGE_STATUS:
    case CHANGE_USERNAME:
     return Object.assign({}, state, payload);

   default: return state;
 }
};

即使使用額外的空格和注釋,這個版本也是很短的,這僅僅是兩個例子.action越多,代碼減少的越多.

switch… case安全嗎?我看到飛流直下的瀑布!(譯注:琢磨了兩天,才明白作者是這個意思,原文-I see a fall through!)

你可能在其他地方讀到過switch聲明應該被避免,尤其是要避免偶然出現的瀑布一樣的流程,因為cases的列表會變得很臃腫.可能你聽說過了,不要刻意的使用瀑布式的代碼,因為捕獲瀑布流的bug非常難.這是個不錯的建議,那就讓我們仔細考慮一下上面提到的危險.

  • Reducers是可以組合的,所以case的臃腫不是問題,如果case的列表變的很大,打碎成片段轉移到分離的reducers中.
  • 每一個case體都會返回一個對象,如此一來瀑布流就不會出現了.一個瀑布流不應該出現一個以上的異常捕獲語句

Redux使用switch..case.只要你遵循簡單原則(保持switches語句體積小,目標集中,在每個case都盡早的返回).swith語句是非常好的.

你可能注意到這個版本需要一個不同籌載(payload).這里就是你的action Creator的發源地

 export const changeStatus = (statusMessage = 'Online') => ({
 type: CHANGE_STATUS,
 payload: { statusMessage }
});

export const changeUserName = (userName = 'Anonymous') => ({
 type: CHANGE_USERNAME,
 payload: { userName }
});

如你所見,這些action creators 把參數和state的結構改變聯系起來了,他是作為一個翻譯的角色.

8.在文檔中使用ES6的參數默認值

如果你正在編輯器中使用Tern.js插件,他將會讀取這些ES6的默認值,在你的action creators中需要的時候引用他們,所以當你調用他們的時候,可以感知他們和執行自動完成.這會減少程序員的認知負擔,因為他們不需要記住所有的載荷雷翔或者檢查他們記不起來的源代碼.

如果你沒有使用類型應用插件例如:Tern,TypeScript或者Flow,是時候使用他們了.

//這一部分實在是不會翻譯了,留下來
 Note: I prefer to rely on inference provided by default assignments visible in the function signature as opposed to type annotations, because:
You don’t have to use a Flow or TypeScript to make it work: Instead you use standard JavaScript.
If you are using TypeScript or Flow, annotations are redundant with default assignments, because both TypeScript and Flow infer the type from the default assignment.
I find it a lot more readable when there’s less syntax noise.
You get default settings, which means, even if you’re not stopping the CI build on type errors (you’d be surprised, lots of projects don’t), you’ll never have an accidental `undefined` parameter lurking in your code.

9. 使用Selectors來計算和解耦和State.

設想你正在構建歷史上最復雜的聊天app.你已經寫了500K的代碼,然后產品團隊拋出一個需要你必須改變state數據結構的新需求.

不要痛苦,你可以很靈巧的使用selectors來從整個State中解耦和app的其余部分.子彈:躲開

黑客帝國,LEO躲開子彈
黑客帝國,LEO躲開子彈

對于我寫過的幾乎每一個reducer,我都創建了一個selector簡單的輸出我需要在視圖中構建的所需要的變量.讓我們看看簡單的chat reducer

   export const getViewState = state =>      Object.assign({}, state);

是的.我知道太簡單了,不值得一看.你可能認為我現在瘋了,但是記起了我們之前多個的子彈了嗎?如果我想添加一下計算state,例如所有會話中我交談過的用戶的完整列表?讓我們叫做recentlyActiveUsers.

這個信息已經存儲在我們當前的state中-但是不太容易得到.讓我們往前看,在getViewState()中獲取他.

 export const getViewState = state => Object.assign({}, state, {
// return a list of users active during this session
recentlyActiveUsers: [...new Set(state.chatLog.map(chat => chat.user))]
});

如果你把所有的計算state都放在selector中,你:

  1. 減少了reducers和組件的復雜想
  2. 把你的app從state的結構中解耦出來.
  3. 遵守單一來源原則,甚至是在reducer中也是這樣.

10.使用TDD:測試優先

很多研究比較了編寫之前測試和編寫之后測試的方法以及根本不測試的方法.結論是清除和顯著的,在實施編寫之前,測試可以減少40-80%的bug.

TDD can effectively cut your shipping bug density in half, and there’s plenty of evidence to back up that claim.

在寫這個文章中的示例時,我都以單元測試開始.

為了避免碎片化測試,我創建了如下的工廠來生產expections:

 const createChat = ({
 id = 0,
 msg = '',
 user = 'Anonymous',
 timeStamp = 1472322852680
} = {}) => ({
 id, msg, user, timeStamp
});

const createState = ({
 userName = 'Anonymous',
 chatLog = [],
 statusMessage = 'Online',
 currentChat = createChat()
} = {}) => ({
 userName, chatLog, statusMessage, currentChat
});

** 注意這兩個測試我都使用了默認值,意思是我可以越過屬性,單獨為我感興趣的測試提供數據**

——

這里是我使用的:

 describe('chatReducer()', ({ test }) => {
 test('with no arguments', ({ same, end }) => {
   const msg = 'should return correct default state';

   const actual = reducer();
   const expected = createState();

   same(actual, expected, msg);
   end();
 });
});

Note: 我使用tape來進項單元測試,因為他夠簡單.我也有2-3年使用Mocha和Jasmine的經驗,以及其他的框架的零散經驗.你需要根據這些原則找到合適的測試框架
注意我在測試標書巢式測試時使用的風格.可能由于我使用過Jasmine和Mocha框架的背景,我喜歡由外部代碼塊開始描述需要測試的組件,接著才是內部的代碼塊.在測試代碼塊內部,我使用簡單的相等斷言,也就是你的測試框架中的deepEqual()或者toEqual()函數.

如你所見,我使用分離的測試聲明和工廠函數來代替像beforeEachafterEach()這樣的工具,這么工具誘導沒有經驗的開發者在測試組件中使用共享的state來完成測試(這個做法不太好).

可能你會猜到,我已經為每個reducer準備了三種不同的測試:

  1. 直接的reducer測試,你可以在例子中看到.這些方法簡單的測試reducer能否產生預期的默認state.
  2. Action creator測試,通過使用預先設定好的sate作為起始點,對每一個action應用reducer來測試action的功能
  3. Selectors測試,測試每個selectors,確保每個預期的屬性的預期值都存在,包括經過計算的屬性.

你已經看到了一個reducer測試,讓我們看看其他的例子

Action Creators Test:

 describe('addChat()', ({ test }) => {
 test('with no arguments', ({ same, end}) => {
   const msg = 'should add default chat message';

   const actual = pipe(
     () => reducer(undefined, addChat()),
     // make sure the id and timestamp are there,
     // but we don't care about the values
     state => {
       const chat = state.chatLog[0];
       chat.id = !!chat.id;
       chat.timeStamp = !!chat.timeStamp;
       return state;
     }
   )();

   const expected = Object.assign(createState(), {
     chatLog: [{
       id: true,
       user: 'Anonymous',
       msg: '',
       timeStamp: true
     }]
   });

   same(actual, expected, msg);
   end();
 });


 test('with all arguments', ({ same, end}) => {
   const msg = 'should add correct chat message';

   const actual = reducer(undefined, addChat({
     id: 1,
     user: '@JS_Cheerleader',
     msg: 'Yay!',
     timeStamp: 1472322852682
   }));
   const expected = Object.assign(createState(), {
     chatLog: [{
       id: 1,
       user: '@JS_Cheerleader',
       msg: 'Yay!',
       timeStamp: 1472322852682
     }]
   });

   same(actual, expected, msg);
   end();
 });
});

這個例子非常的有意思,原因有幾個.addChat() action creator是不純的.意思是除非你傳遞值代替原值,否則你就獲得不了預期的屬性值.為了對付這個問題,我使用了管道.我有時使用管道來避免創建了不需要的附加值.又是使用它來忽略生成的值.我仍然卻行他們是存在的,但是我不關心這些值到底是什么.注意我甚至都沒有檢查type類型.我依靠類型引用和默認值來完成和這個任務.

一個管道(pipe)是一個工具函數,讓你通過一系列的函數傳遞一些輸入值,這些系列函數都接受之前函數的輸出值,之后做出某種程度的變化.我使用了loadh/fp/pipe,別名是loadsh/flow.有意思的是pipe()也可以在reducer函數中創建.

 const pipe = (...fns) => x => fns.reduce((v, f) => f(v), x);

const fn1 = s => s.toLowerCase();
const fn2 = s => s.split('').reverse().join('');
const fn3 = s => s + '!'

const newFunc = pipe(fn1, fn2, fn3);
const result = newFunc('Time'); // emit!

我更愿意在reducers文件中使用pipe()來簡化state的變化.所有的state的變化最終都從一個數數據流都另個數據流.pipe()很擅長這個工作.

注意,action creator讓我們忽略所有的默認值,所以我們可以傳遞特定的ids和時間戳,可以測試特殊的值.

Selectors測試

最后,我們來測試一下state selectors.確保經過計算的值是正確的.要做的是:

 describe('getViewState', ({ test }) => {
  test('with chats', ({ same, end }) => {
    const msg = 'should return the state needed to render';
    const chats = [
      createChat({
        id: 2,
        user: 'Bender',
        msg: 'Does Barry Manilow know you raid his wardrobe?',
        timeStamp: 451671300000
      }),
      createChat({
        id: 2,
        user: 'Andrew',
        msg: `Hey let's watch the mouth, huh?`,
        timeStamp: 451671480000 }),
      createChat({
        id: 1,
        user: 'Brian',
        msg: `We accept the fact that we had to sacrifice a whole Saturday in
              detention for whatever it was that we did wrong.`,
        timeStamp: 451692000000
      })
    ];

    const state = chats.map(addChat).reduce(reducer, reducer());

    const actual = getViewState(state);
    const expected = Object.assign(createState(), {
      chatLog: chats,
      recentlyActiveUsers: ['Bender', 'Andrew', 'Brian']
    });

    same(actual, expected, msg);
    end();
  });
});

注意,在這個測試中,我們使用了JS數組原型方法reducer()來reducer 累積一些actions addChat()的值.Redux reducer非常好的一個地方是,他們僅調控reducer函數,你可以使用reducer做任何其他reducer能做的事情.

我們的expected值檢查了所有日志中的chat對象以及最近的活躍用戶的列表是否正確.

沒有什么要說的了.

Redux的軍規

如果你正確的使用Redux,你會獲得很多大好處:

  • 減少時間依賴的bugs
  • 確定性的視圖渲染
  • 確定性的state重演
  • 容易實施的undo/redo特性
  • 簡單的debug
  • 成為一個時間旅行者

但是為了保證上面的功能可以正常工作,你還要記住以下的規則:

  • Reducer必須是純函數
  • Reducer必須是state的唯一來源
  • Reducer的state應該總是被序列化
  • Reducer state不能包含有函數

還要牢記于心:

  • 不是所有的app都需要Redux
  • 用常量定義action Types
  • 使用action creators來解耦 action邏輯和dispatch的調用
  • 使用ES6的默認參數方法來描述參數特征
  • 使用selectors來計算state和解耦
  • 一定要使用TDD(譯注:馬上回考慮的)

祝你愉快!

譯文完


媽呀,5000多字,手指都敲掉皮了.沒功勞也有苦勞啊,看到這里給個??吧.

Members of “Learn JavaScript with Eric Elliott”, check out the new functional programming and Redux lessons. Be sure to watch the Shotgun series & ride shotgun with me while I build real apps with React and Redux.
Not a member? Join Today!
Eric Elliott is the author of “Programming JavaScript Applications” (O’Reilly), and “Learn JavaScript with Eric Elliott”. He has contributed to software experiences for Adobe Systems, Zumba Fitness, The Wall Street Journal, ESPN, BBC, and top recording artists including Usher, Frank Ocean, Metallica, and many more.
He spends most of his time in the San Francisco Bay Area with the most beautiful woman in the world.

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,238評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,430評論 3 415
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,134評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,893評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,653評論 6 408
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,136評論 1 323
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,212評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,372評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,888評論 1 334
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,738評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,939評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,482評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,179評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,588評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,829評論 1 283
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,610評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,916評論 2 372

推薦閱讀更多精彩內容