1. React+Redux項目結構最佳實踐
2. 如何合理地設計Redux的State前端
在前面兩篇文章中,咱們介紹了Redux項目結構的組織方式和如何設計State。本篇,咱們將之前面兩篇文章爲基礎,繼續介紹如何設計action、reducer、selector。sql
依然以博客項目爲例,咱們在第2篇中最後設計的state結構以下:數據庫
{ "app":{ "isFetching": false, "error": "", }, "posts":{ "byId": { "1": { ... }, ... }, "allIds": [1, ...], } "comments": { ... }, "authors": { ... } }
根據這個結構,咱們很容易想到能夠拆分紅4個reducer分別處理app、posts、comments、authors這4個子state。子state相關的action和這個state對應的reducer放到一個文件中,做爲一個state處理模塊。注意:本文定義的action、reducer、selector並不涵蓋真實博客應用中涉及的全部邏輯,僅列舉部分邏輯,用以介紹如何設計action、reducer、selector。json
state中的 app 管理應用狀態,應用狀態與領域狀態不一樣,領域狀態是應用用來顯示、操做的數據,通常須要從服務器端獲取,例如posts、comments、authors都屬於領域狀態;而應用狀態是與應用行爲或應用UI直接相關的狀態,例如當前應用中是否正在進行網絡請求,應用執行時的錯誤信息等。app 包含的應用狀態有:isFetching(當前應用中是否正在進行網絡請求)和error(應用執行時的錯誤信息)。對應的action能夠定義爲:redux
// 所在文件:app.js //action types export const types = { const START_FETCH : 'app/START_FETCH', const FINISH_FETCH : 'app/FINISH_FETCH', const SET_ERROR : 'app/SET_ERROR' } //action creators export const actions = { startFetch: () => { return {type: types.START_FETCH}; }, finishFetch: ()=> { return {type: types.FINISH_FETCH}; }, setError: (error)=> { return {type: types.SET_ERROR, payload: error}; } }
types定義了app模塊使用的action types,每個action type的值以模塊名做爲命名空間,以免不一樣模塊的action type衝突問題。actions定義了該模塊使用到的action creators。咱們沒有直接導出每個action type和action creator,而是把全部的action type封裝到types常量,全部的action creators封裝到actions常量,再導出types和actions這兩個常量。這樣作的好處是方便在其餘模塊中引用。(在第1篇中已經介紹過)
如今再來定義處理app的reducer:segmentfault
// 所在文件:app.js export const types = { //... } export const actions = { //... } const initialState = { isFetching: false, error: null, } // reducer export default function reducer(state = initialState, action) { switch (action.type) { types.START_FETCH: return {...state, isFetching: true}; types.FINISH_FETCH: return {...state, isFetching: false}; types.SET_ERROR: return {...state, error: action.payload} default: return state; } }
如今,app.js就構成了一個基本的處理state的模塊。服務器
咱們再來看下如何設計posts.js。posts是這幾個子狀態中最複雜的狀態,包含了posts領域數據的兩種組織方式:byId定義了博客ID和博客的映射關係,allIds定義了博客在界面上的顯示順序。這個模塊須要使用異步action調用服務器端API,獲取博客數據。當網絡請求開始和結束時,還須要使用app.js模塊中的actions,用來更改app中的isFetching狀態。代碼以下所示:網絡
// 所在文件:posts.js import {actions as appActions} from './app.js' //action types export const types = { const SET_POSTS : 'posts/SET_POSTS', } //action creators export const actions = { // 異步action,須要redux-thunk支持 getPosts: () => { return (dispatch) => { dispatch(appActions.startFetch()); return fetch('http://xxx/posts') .then(response => response.json()) .then(json => { dispatch(actions.setPosts(json)); dispatch(appActions.finishFetch()); }); } }, setPosts: (posts)=> { return {type: types.SET_POSTS, payload: posts}; } } // reducer export default function reducer(state = [], action) { switch (action.type) { types.SET_POSTS: let byId = {}; let allIds = []; /* 假設接口返回的博客數據格式爲: [{ "id": 1, "title": "Blog Title", "create_time": "2017-01-10T23:07:43.248Z", "author": { "id": 81, "name": "Mr Shelby" }, "comments": [{id: 'c1', authorId: 81, content: 'Say something'}] "content": "Some really short blog content. " }] */ action.payload.each((item)=>{ byId[item.id] = item; allIds.push(item.id); }) return {...state, byId, allIds}; default: return state; } }
咱們在一個reducer函數中處理了byId和allIds兩個狀態,當posts的業務邏輯較簡單,須要處理的action也較少時,如上面的例子所示,這麼作是沒有問題的。但當posts的業務邏輯比較複雜,action類型較多,byId和allIds響應的action也不一致時,每每咱們會拆分出兩個reducer,分別處理byId和allIds。以下所示:app
// 所在文件:posts.js import { combineReducers } from 'redux' //省略無關代碼 // reducer export default combineReducers({ byId, allIds }) const byId = (state = {}, action) { switch (action.type) { types.SET_POSTS: let byId = {}; action.payload.each((item)=>{ byId[item.id] = item; }) return {...state, byId}; SOME_SEPCIAL_ACTION_FOR_BYID: //... default: return state; } } const allIds = (state = [], action) { switch (action.type) { types.SET_POSTS: return {...state, allIds: action.payload.map(item => item.id)}; SOME_SEPCIAL_ACTION_FOR_ALLIDS: //... default: return state; } }
從上面的例子中,咱們能夠發現,redux的combineReducers能夠在任意層級的state上使用,而並不是只能在第一級的state上使用(示例中的第一層級state是app、posts、comments、authors)。異步
posts.js模塊還有一個問題,就是byId中的每個post對象,包含嵌套對象author。咱們應該讓post對象只應用博客做者的id便可:
// reducer export default function reducer(state = [], action) { switch (action.type) { types.SET_POSTS: let byId = {}; let allIds = []; action.payload.each((item)=>{ byId[item.id] = {...item, author: item.author.id}; allIds.push(item.id); }) return {...state, byId, allIds}; default: return state; } }
這樣,posts只關聯博客做者的id,博客做者的其餘屬性由專門的領域狀態author來管理:
// 所在文件:authors.js import { types as postTypes } from './post' //action types export const types = { } //action creators export const actions = { } // reducer export default function reducer(state = {}, action){ switch (action.type) { postTypes.SET_POSTS: let authors = {}; action.payload.each((item)=>{ authors[item.author.id] = item.author; }) return authors; default: return state; }
這裏須要注意的是,authors的reducer也處理了posts模塊中的SET_POSTS這個action type。這是沒有任何問題的,一個action自己就是能夠被多個state的reducer處理的,尤爲是當多個state之間存在關聯關係時,這種場景更爲常見。
comments.js模塊的實現思路相似,再也不贅述。如今咱們的redux(放置redux模塊)目錄結構以下:
redux/ app.js posts.js authors.js comments.js
在redux目錄層級下,咱們新建一個index.js文件,用於把各個模塊的reducer合併成最終的根reducer。
// 文件名:index.js import { combineReducers } from 'redux'; import app from './app'; import posts from './posts'; import authors from './authors'; import commments from './comments'; const rootReducer = combineReducers({ app, posts, authors, commments }); export default rootReducer;
action和reducer的設計到此基本完成,下面咱們來看selector。Redux中,selector的「名聲」不如action、reducer響亮,但selector其實很是有用。selector是用於從state中獲取所需數據的函數,一般在connect的第一個參數 mapStateToProps中使用。例如,咱們在AuthorContainer.js中根據做者id獲取做者詳情信息,不使用selector的話,能夠這麼寫:
//文件名:AuthorContainer.js //省略無關代碼 function mapStateToProps(state, props) { return { author: state.authors[props.authorId], }; } export default connect(mapStateToProps, mapDispatchToProps)(AuthorContainer);
這個例子中,由於邏輯很簡單,直接獲取author看起來沒什麼問題,但當獲取狀態的邏輯變得複雜時,須要經過一個函數來獲取,這個函數就是一個selector。selector是能夠複用的,不一樣的容器組件,只要獲取狀態的邏輯相同,就能夠複用一樣的selector。因此,selector不能直接定義在某個容器組件中,而應該定義在其關聯領域所在的模塊中,這個例子須要定義在authors.js中。
//authors.js //action types //action creators //reducer // selectors export function getAuthorById(state, id) { return state[id] }
在AuthorContainer.js中使用selector:
//文件名:AuthorContainer.js import { getAuthorById } from '../redux/authors'; //省略無關代碼 function mapStateToProps(state, props) { return { author: getAuthorById(state.authors, props.authorId), }; } export default connect(mapStateToProps)(AuthorContainer);
咱們再來看一個複雜些的selector:獲取一篇博客的評論列表。獲取評論列表數據,須要posts和comments兩個領域的數據,因此這個selector並不適合放到comments.js模塊中。當一個selector的計算參數依賴多個狀態時,能夠把這個selector放到index.js中,咱們把index.js看作全部模塊層級之上的一個根模塊。
// index.js // 省略無關代碼 // selectors export function getCommentsByPost(post, comments) { const commentIds = post.comments; return commentIds.map(id => comments[id]); }
咱們在第2篇 如何合理地設計Redux的State講過,要像設計數據庫同樣設計state,selector就至關於查詢表的sql語句,reducer至關於修改表的sql語句。因此,本篇的總結是:像寫sql同樣,設計和組織action、reducer、selector。
歡迎關注個人公衆號:老幹部的大前端,領取21本大前端精選書籍!