翻譯|Redux-Starter-Kit 使用指南

原文在這裏, 這是Redux官方的充電包工具集,可使Redux的用法簡單一點. 本文是文檔中的用戶指南的翻譯版本 [^譯註:這裏的包裝只是簡化了操做,概念性問題的難度並無下降,所以在沒有理解Redux核心概念以前,Redux的文檔任然是學習的敲門磚]javascript

前言

Redux核心庫刻意安排爲沒有任何偏向性.所以可讓使用者本身來處理每一個問題,例如包含了state的State的配置,以及怎麼構建reducers. 在某些使用案例中,是很好的,由於這麼樣作給了你很大的靈活性,可是用戶並不老是須要靈活性.有時候,咱們須要的只是盡肯能簡單的開始工做,只須要有開箱即用的默認配置就能夠.又或者是,你正在編寫一個大型的應用,發現本身寫了太多相似的代碼,你很是但願能山減掉大量的手寫代碼.java

正如在快速入門中講到的同樣,Redux Starter Kit的目標是協助簡化Redux的常見使用用例.它並無像你想象的那樣成爲一個完整的解決方案,而是使得一些Redux相關代碼的編寫變簡單(或者在某些狀況下,不折不扣的減小手寫代碼的量).git

Redux Starter Kit 導出了幾個可供使用的獨立函數,在其餘的包中添加依賴就能夠和Redux一塊兒工做.這可讓你決定,究竟是在全新的項目仍是已經進行的項目中使用Kit.github

接下來看看一些Redux Starter Kit的用法,這些用法可讓你的Redux代碼更漂亮.redux

Store的配置

每一個Redux App須要配置並建立一個Store,一般狀況下包含如下幾個步驟:api

  • 導入或者建立頂層reducer函數(root reducer function)
  • 配置中間件(middleware),例如包含至少一個有關異步操做邏輯的中間件
  • 配置Redux DevTools 擴展
  • 盡你所能的配置一些專門用於開發環境或者產品環境的切換邏輯

Store的手工配置

下面的代碼是 Redux文檔 ‌配置Store中的典型代碼數組

import { applyMiddleware, compose, createStore } from 'redux'
import { composeWithDevTools } from 'redux-devtools-extension'
import thunkMiddleware from 'redux-thunk'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureStore(preloadedState) {
 const middlewares = [loggerMiddleware, thunkMiddleware]
 const middlewareEnhancer = applyMiddleware(...middlewares)

 const enhancers = [middlewareEnhancer, monitorReducersEnhancer]
 const composedEnhancers = composeWithDevTools(...enhancers)

 const store = createStore(rootReducer, preloadedState, composedEnhancers)

 if (process.env.NODE_ENV !== 'production' && module.hot) {
   module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
 }

 return store
}
複製代碼

實例代碼的可讀性還能夠,可是流程不太直接明瞭:app

  • 基礎的Redux createStore函數接收幾個固定位置的參數:(rootReducer,preloadedState,enhancer).有時候很容易忘記其中的參數.
  • 中間件(middleware)和加強件(enhancer)的設定過程使人感到困惑,尤爲是你準備添加多個配置的時候.
  • Redux DevTools 擴展文旦初始建議你使用一些手寫文檔檢查全局做用域中可用的擴展.不少用戶只是簡單的拷貝粘貼這些代碼塊,這使得配置代碼很難理解.

使用configureStore函數簡化Store的配置

configureStore在一下幾個方面對咱們有幫助:異步

  • 有一個"name"參數的可選對象,很容易讀懂.
  • 容許你提供中間件和加強件的數組,用於在store中添加這些組件,並自動調用applyMiddlewarecompose函數.
  • 自動開啓Redux DevTools擴展

此外,confitureStore默認添加了一些有特定用途的中間件:ide

  • redux-thunk 是在組件外執行同步和異步邏輯的經常使用中間件
  • 在開發環境,用於檢查常見state mutate操做或者使用非序列化錯誤的中間件.

這麼作意味着,store的配置代碼自己更短,更容易閱讀.

最簡答的用法是隻要把頂層reducer函數做爲reducer 形參傳遞就能夠了.

import { configureStore } from 'redux-starter-kit'
import rootReducer from './reducers'

const store = configureStore({
reducer: rootReducer
})

export default store
複製代碼

也能夠傳遞分片(slice)的reducer,configureStore會自動調用combineReducers:

import usersReducer from './usersReducer'
import postsReducer from './postsReducer'

const store = configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer
  }
})
複製代碼

注意,這個用法只對第一級的reducer有用.若是你想嵌套reducer,須要本身調用combineReducers來完成更低一級的嵌套.

若是你想定製store的配置,能夠傳遞額外的選項.這裏是熱加載的實例:

import { configureStore, getDefaultMiddleware } from 'redux-starter-kit'

import monitorReducersEnhancer from './enhancers/monitorReducers'
import loggerMiddleware from './middleware/logger'
import rootReducer from './reducers'

export default function configureAppStore(preloadedState) {
  const store = configureStore({
    reducer: rootReducer,
    middleware: [loggerMiddleware, ...getDefaultMiddleware()],
    preloadedState,
    enhancers: [monitorReducersEnhancer]
  })

  if (process.env.NODE_ENV !== 'production' && module.hot) {
    module.hot.accept('./reducers', () => store.replaceReducer(rootReducer))
  }

  return store
}
複製代碼

若是你提供了middleware參數, confitureStore就只使用你提供的中間件. 若是你想在默認中間件基礎上添加定製的中間件,能夠調用getDefaultMiddleware,把你本身的中間件數組包含進去.

編寫Reducers

Reducers是Redux中最重要的概念,一個典型的reducer函數須要具有的功能是:

  • 查找action對象的type 字段,來決定如何響應action
  • 經過拷貝state中須要修改的部分,只修改這些部分,從而不可突變的更新Redux的state.

當你在reducer中使用須要的條件邏輯是,最多見的方法是switch聲明,由於針對單個字段執行最直接的操做.然而不少人不喜歡switch聲明.Redux文檔展現了基於action type類型的映射的用法,可是須要你本身配置.

另外一個常見的痛點是編寫reducers時,不要有不可突變的更新state.Javascript是可突變的語言.手動更新嵌套的數據很是棘手,很容易出錯.

使用createReducer函數簡化Reducer的操做

由於"查表"(映射)方法很流行,Redux Starter Kit 包含了一個相似Redux 文檔中createReducer的函數.然而咱們的createReducer工具備一些魔法,是的Reducer的操做更好,在內部它使用'Immer'庫,Immer庫可讓你編寫假的"突變"代碼,實際上進行了不可突變動新.這麼作有效的避免了偶然的突變操做.

整體上,任何使用switch聲明的Reducx Reducer均可以直接轉化使用createReducer. switch中每一個case都變成傳遞給createReducer對象的一個鍵. 不可突變動新邏輯,例如對象展開操做,拷貝數組均可以直接轉換爲"可突變"操做 . 保持原來的不可突變操做也能夠, 只需返回更新的拷貝就行.

這裏有一些可使用createReducer的實例. 咱們從經典的"todo list" reducer開始,使用的是switch聲明和不可突變動新

function todosReducer(state = [], action) {
    switch(action.type) {
        case "ADD_TODO": {
            return state.concat(action.payload);
        }
        case "TOGGLE_TODO": {
            const {index} = action.payload;
            return state.map( (todo, i) => {
                if(i !== index) return todo;

                return {
                    ...todo,
                    completed : !todo.completed
                };
            });
        } ,
        "REMOVE_TODO" : (state, action) => {
            return state.filter( (todo, i) => i !== action.payload.index)
        }
        default : return state;
    }
}
複製代碼

注意:咱們聲明調用了state.concat(),返回包含todo新條目的通過拷貝的數組,state.map() 返回toggle分支的拷貝數組,這裏使用對象展開操做符對要更新的todo項進行了複製.

經過使用createReducer,咱們能夠考慮簡化實例:

const todosReducer = createReducer([], {
    "ADD_TODO" : (state, action) => {
        // "mutate" the array by calling push()
        state.push(action.payload);
    },
    "TOGGLE_TODO" : (state, action) => {
        const todo = state[action.payload.index];
        // "mutate" the object by overwriting a field
        todo.completed = !todo.completed;
    },
    "REMOVE_TODO" : (state, action) => {
        // Can still return an immutably-updated value if we want to
        return state.filter( (todo, i) => i !== action.payload.index)
    }
})
複製代碼

"突變"state的能力在試圖更新深度嵌套的state時特別有用.複雜而使人痛苦的代碼以下:

case "UPDATE_VALUE":
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
複製代碼

能夠簡化爲下面的代碼:

updateValue(state, action) {
    const {someId, someValue} = action.payload;
    state.first.second[someId] = someValue;
}
複製代碼

看上去好多了 [^譯註:這裏看上去是可突變操做,實際底層使用的Immer在操做以前已經進行了拷貝操做,不會在原始內存地址作修改].

以對象的形式定義函數

在現代Javascript中,有幾個固定好的方法能夠在對象中定義鍵和函數(並非特定針對Redux),因此你能夠混合匹配不一樣的鍵定義和函數定義. 例以下面對象中全部的函數定義方法都是合規的.

const keyName = "ADD_TODO4";

const reducerObject = {
    // Explicit quotes for the key name, arrow function for the reducer
    "ADD_TODO1" : (state, action) => { }

    // Bare key with no quotes, function keyword
    ADD_TODO2 : function(state, action){  }

    // Object literal function shorthand
    //對象字面量函數簡寫方式
    ADD_TODO3(state, action) { }

    // Computed property
    [keyName] : (state, action) => { }
}
複製代碼

使用對象字面量函數簡寫方式 多是最簡短的代碼,可是你可使用以上任何一種方式.

使用createReducer要考慮的因素

Redux Starter Kit的createReducer函數能夠發揮很大的做用,可是也要留心:

  • "突變"的代碼只能在createReducer函數內使用
  • Immer不容許混合突變(譯註:這裏指的是真正的JS突變操做)操做,又返回新的state.

查看createReducer API 參考 ,瞭解具體的詳情.

編寫 Action Creators

Redux鼓勵使用者編寫action creator 函數,action creator函數封裝了建立Action 對象的流程. 可是標準的Redux用法中不是必須的.

絕大多數的action creator都很是簡單.它們接受一些參數,返回一個擁有特定type字段和傳入參數的 action對象.這些參數一般放在payload字段下, action隊形是‌Flux標準action的傳統定義,目的是組織action對象的內容. 一個典型action creator的結構以下:

function addTodo(text) {
 return {
   type: 'ADD_TODO',
   payload: { text }
 }
}
複製代碼

使用createAction函數定義Action Creators

手動編寫action creators很乏味.Redux Starter Kit提供了一個函數 createAction,用於簡化給定了action type的action對象的建立過程,傳入的參數安置在payload字段中:

const addTodo = createAction('ADD_TODO')
addTodo({ text: 'Buy milk' })
// {type : "ADD_TODO", payload : {text : "Buy milk"}})
複製代碼

目前,creatAction不容許你定製payload字段. 必需要把payload做爲整個參數來傳遞.payload 能夠是單個的值,或者一個有大量數據的對象(咱們最終會爲‌createAction添加一個用於定製payload的回調函數,或者是能夠添加其餘相似meta同樣的字段).

把Action Creatros 做爲Action Type

Redux的reducers函數須要查找特定的action types來決定該如何更新state.一般狀況下, 定義action type 字符串和定義action creator函數式分開的. Redux Starter Kit的createAction函數使用了一套技巧讓兩個定義更容易了.

首先,createAction 在它生成的action creators上 重寫了toString()方法. 意思是,action creators自身也能夠做爲"action type"來引用,例如能夠做爲creareReducer的鍵.

第二點,action type也能夠定義爲action creator的一個type 字段.

const actionCreator = createAction("SOME_ACTION_TYPE");

console.log(actionCreator.toString())
// "SOME_ACTION_TYPE"

console.log(actionCreator.type);
// "SOME_ACTION_TYPE"

const reducer = createReducer({}, {
    // actionCreator.toString() will automatically be called here
    [actionCreator] : (state, action) => {}

    // Or, you can reference the .type field:
    [actionCreator.type] : (state, action) => { }
});
複製代碼

這意味着,你不須要編寫或使用獨立的action type變量,或或者重複action type的名字和值,例如const SOME_ACTION_TYPE = "SOME_ACTION_TYPE" 就不須要了.

不幸的是,隱式轉換爲字符串對於switch聲明不起做用.若是你在一個switch 聲明中使用了這些action creator,須要你本身調用actionCreator.toString()

const actionCreator = createAction('SOME_ACTION_TYPE')

const reducer = (state = {}, action) => {
  switch (action.type) {
    // ERROR: 這裏會出錯, 在switch中,actionCreator不會自動獲取字符串
    case actionCreator: {
      break
    }
    // CORRECT: this will work as expected
    case actionCreator.toString(): {
      break
    }
    // CORRECT: this will also work right
    case actionCreator.type: {
      break
    }
  }
}
複製代碼

若是你同時使用了Redux Starter Kit和TypeScript,要留心, 在action creator 用於對象的鍵時,TypeScript編譯器不會接受 toString()的隱式轉換.這種狀況下,你須要手動轉換(actionCreator as string),或者使用.type字段做爲鍵.

建立 分片的State,

Redux 的state一般都以"切片"的形式組織,由reducers定義的切片 state傳遞給combineReducers.

import { combineReducers } from 'redux'
import usersReducer from './usersReducer'
import postsReducer from './postsReducer'

const rootReducer = combineReducers({
  users: usersReducer,
  posts: postsReducer
})
複製代碼

在這個例子中,usersposts都被認爲是"切片(slices)".也都是reducers:

  • "擁有"state中的一塊, 包括初始值
  • 定義了state更新的方法
  • 定義了會引發state更新的特定Action

常規的方法是在本身獨立的文件中定義切片reducer函數,action creators在第二個文件中.由於兩個文件都須要引用一樣的action types, 又會在第三個文件中定義,並在前兩個文件中導入:

// postsConstants.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'

// postsActions.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'

export function addPost(id, title) {
 return {
   type: CREATE_POST,
   payload: { id, title }
 }
}

// postsReducer.js
import { CREATE_POST, UPDATE_POST, DELETE_POST } from './postConstants'

const initialState = []

export default function postsReducer(state = initialState, action) {
 switch (action.type) {
   case CREATE_POST: {
     // omit implementation
   }
   default:
     return state
 }
}
複製代碼

這惟一必不可少的部分只有reducer本身,想一想其餘部分:

  • 咱們能夠在兩個地方編寫action type 做爲行內字符串.
  • action creators很好,可是對於使用Redux不是必須的,一個組件能夠直接跳過由connect提供的mapDispatch參數,轉而直接使用this.porps.dispatch({type:'CREATE_POST',payload:{id:123,title:"Hello World"}})
  • 惟一要這麼寫的緣由是由於這是常規的作法.

"‌鴨式"文件結構 建議把全部的Redux相關邏輯放到一個單一文件中,像這樣:

// postsDuck.js
const CREATE_POST = 'CREATE_POST'
const UPDATE_POST = 'UPDATE_POST'
const DELETE_POST = 'DELETE_POST'

export function addPost(id, title) {
return {
 type: CREATE_POST,
 payload: { id, title }
}
}

const initialState = []

export default function postsReducer(state = initialState, action) {
switch (action.type) {
 case CREATE_POST: {
   // Omit actual code
   break
 }
 default:
   return state
}
}
複製代碼

這樣作簡化了不少事,由於咱們不須要多個文件,因此能夠移除掉冗餘的action type 常量的導入.可是,咱們仍然要手動編寫action types和action creators.

使用createSlice簡化Slices(切片)的建立

爲了簡化這個過程,Redux Starter Kit 包含了一個createSlice函數, 能夠根據你提供的reducer函數的名字自動生成action type和action creators.

這裏是pops的實例代碼:

const postsSlice = createSlice({
  initialState: [],
  reducers: {
    createPost(state, action) {},
    updatePost(state, action) {},
    deletePost(state, action) {}
  }
})

console.log(postsSlice)
/* { actions : { createPost, updatePost, deletePost, }, reducer } */

const { createPost } = postsSlice.actions

console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "createPost", payload : {id : 123, title : "Hello World"}}
複製代碼

createSlice查找全部在reducers字段中定義的函數,以及每個"case reducer"函數,用reducer的名字生成同名的action creator和action type.因此, createPost reducer 將會返回 一個createPost類型,和createPost() action creator.

也能夠選擇性定義一個slice參數用於action type的前綴:

const postsSlice = createSlice({
  slice: 'posts',
  initialState: [],
  reducers: {
    createPost(state, action) {},
    updatePost(state, action) {},
    deletePost(state, action) {}
  }
})

const { createPost } = postsSlice.actions

console.log(createPost({ id: 123, title: 'Hello World' }))
// {type : "posts/createPost", payload : {id : 123, title : "Hello World"}}
複製代碼

導出並使用Slices

大多數狀況下,須要定義一個slice,導出action creators和reducer. 推薦的方法是使用ES6的解構和導出方法:

const postsSlice = createSlice({
  slice: 'posts',
  initialState: [],
  reducers: {
    createPost(state, action) {},
    updatePost(state, action) {},
    deletePost(state, action) {}
  }
})

// Extract the action creators object and the reducer
const { actions, reducer } = postsSlice
// Extract and export each action creator by name
export const { createPost, updatePost, deletePost } = actions
// Export the reducer, either as a default or named export
export default reducer
複製代碼

也能夠導出須要的slice部分.

經過這種方式定義的Slice和 "Redux Ducks pattern"的概念很是相似. 然而,他們也要一些潛在的問題,在導入和導出slices時須要注意.

首先,Redux action types並非說專門只對應一個slice. 從概念上說,每一個slice reducer "擁有"他們本身的一塊state,例如,可是reducer應該能鑑監放任何action type並更新對應的state.例如, 不少不一樣的slice可能都會響應"user logger out" action, 包括清除數據,重置state爲初始值等等.在設計state和建立slices是要特別小心.

第二點,JS模塊在兩個模塊互相引用是會有"循環引用"問題. 這會致使導入變爲未定義, 由此可能會中斷須要導入的代碼, 特別是在"ducks"或者"slices"中,若是連個slice定義在不一樣的問題件中,要響應定義在另外一個文件中斷額action是可能會出問題.

若是你碰到這個問題, 能夠須要重構代碼避免循環引用.

相關文章
相關標籤/搜索