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本大前端精选书籍!