前言:正在学习react大众点评项目课程的todoList应用阶段,学习react、redux、react-router基础知识。
一、Redux项目结构组织方式
1.按照类型
- 当项目变大后非常不方便


2.按照功能模块
- 方便开发,易于功能的扩展。
- 但不同的功能模块的状态可能存在耦合。


3.Ducks鸭子(推荐)


// Action Creators
export const actions = {
loadWidget: function loadWidget() {
return { type: LOAD };
},
createWidget: function createWidget(widget) {
return { type: CREATE, widget };
},
updateWidget: function updateWidget(widget) {
return { type: UPDATE, widget };
},
removeWidget: function removeWidget(widget) {
return { type: REMOVE, widget };
}
}
- view层和状态管理层比较彻底的解耦
- 好处:组件引用这些action时,不需要一个一个去引用action的名字,引用时直接引用一个action
二、State设计原则
1.常见两种错误
- 以API为设计State的依据
- 以页面UI为设计State的依据
2.像设计数据库一样设计StateS
- 设计数据库基本原则
- 数据按照领域(Domain)分类,存储在不同的的表中,不同的表中存储的列数据不能重复
- 表中每一列的数据都依赖于这张表的主键。
- 表中除了主键以外的其他列,互相之间不能有直接依赖关系。
- 设计State原则
- 数据按照领域把整个应用的状态按照领域(Domain)分成若干个子State,子State之间不能保存重复的数据
- 表中State以键值对的结构存储数据,以记录的key/ID作为记录 的索引,记录种的其它字段都依赖于索引
- State中不能保存可以通过已有数据计算而来的数据,即State中的字段不互相依赖
3.有序State
{
"posts": {
"1": {
"id": 1,
"title": "Blog Title",
"content": "Some really short blog content.",
"created_at": "2016-01-11T23:07:43.248Z",
"author": 81,
"comments": [
352
]
}
...
"3": {
...
}
},
"postIds": [1, ..., 3], //保证博客列表的有序性 posts.id主键
避免state对象嵌套层级过深的问题
"comments": {
"352": {
"id": 352,
"content": "Good article!",
"author": 41
},
...
},
"authors": {
"41": {
"id": 41,
"name": "Jack"
},
"81": {
"id": 81,
"name": "Mr Shelby"
},
...
}
}
4.包含UI状态的State, 合并State节点
{
"app":{
"isFetching": false,
"error": "",
},
"posts":{
"byId": {
"1": {
...
},
...
},
"allIds": [1, ...],
}
"comments": {
...
},
"authors": {
...
}
}
- 补充:
- State应该尽量扁平化(避免嵌套层级过深)
- UI State:具有松散性特点(可以考虑合并,避免一级节点数量过多)
三、Selector函数(选择器函数)

作用一:从Redux的state中读取部分数据,将读取到的数据给Container Compoinents使用
使用原因一:实现Containern Components层和Redux的state层的解耦
问题:AddTodoContainer.js中通过对象属性访问的方式获取text属性;
如果text要修改为date,所有使用text的地方都要改
const mapStateToProps = state => ({
text: state.text
});
使用Selector函数解决:
- src->selector->index.js
export const getText = (state) => state.text
- AddTodoContainer.js中通过函数调用的方式获取text属性
import {getText} from "../selectors"
const mapStateToProps = state => ({
text: getText(state)
});
使用原因二:Component代表的view层,和Redux代表的状态层是独立的两个层级;
这两个层级的交互应该是通过接口进行通信的,而不是通过state的数据结构通信。
作用二:对读取到的状态进行计算,然后返回计算后的值
- src->selector->index.js
export const getVisibleTodos = (state) => { const {todos: {data}, filter} = state switch (filter) { case "all": return data; case "completed": return data.filter(t => t.completed); case "active": return data.filter(t => !t.completed); default: return new Error("Unknown filter: " + filter); } }; - TodoListContainer.js
import {getVisibleTodos} from "../selectors" const mapStateToProps = state => ({ todos: getVisibleTodos(state) });
补充:如果selector函数数量非常多,可以拆分为多个文件
四、深入理解前端状态管理思想



五、Middleware(中间件)
示例:redux-thunk
本质:增强store dispatch的能力

默认情况下,dispatch只能处理一个普通对象类型的action
使用redux-thunk后,dispatch可以处理函数类型的action
- middlewares->logger.js
/** * 打印action、state */ const logger = ({getState, dispatch}) => next => action => { console.group(action.type); console.log('dispatching:', action); const result = next(action); console.log('next state:', getState()); console.groupEnd(); return result; } export default logger;

六、store enhancer(store增强器)
增强redux store的功能
createStore(reducer, [preloadedState], [enhancer])

- enhancers->logger.js
/** * 打印actions、state */ const logger = createStore => (...args) => { //args: reducer、初始state const store = createStore(...args); const dispatch = (action) => { console.group(action.type); console.log('dispatching:', action); const result = store.dispatch(action); console.log('next state:', store.getState()); console.groupEnd(); return result; } return {...store, dispatch} } export default logger; -
使用store enhancer:applyMiddleware也是store enhancer,使用compose函数将多个store enhancer连接、组合。
import { createStore, applyMiddleware, compose } from "redux"; import loggerEnhancer from "./enhancers/logger" const store = createStore(rootReducer, compose(applyMiddleware(thunkMiddleware),loggerEnhancer));
中间件是一种特殊的store enhancer
日常使用中:尽可能的使用middleware,而不是store enhancer,因为middleware是一个更高层的抽象。

慎用store enhancer:
- 通过store enhancer不仅可以增强store dispatch的能力,还可以增强其它store中包含的方法,例如getState、subscribe,看似有更大的灵活度对store进行修改,但其实也隐含了一个风险,就是很容易破环store的原有逻辑。
- 一般选择middleware中间件增强store dispatch的能力:因为中间件约束了我们一个行为,让我们难以去改变store的较底层的逻辑
七、常用库集成:Immutable.js
用来操作不可变对象的库
常用的两个不可变的数据结构
const { Map } = require('immutable');
const map1 = Map({ a: 1, b: 2, c: 3 }); //不可变类型的map结构
const map2 = map1.set('b', 50);
map1.get('b') + " vs. " + map2.get('b'); // 2 vs. 50
const { List } = require('immutable');
const list1 = List([ 1, 2 ]);
const list2 = list1.push(3, 4, 5);
const list3 = list2.unshift(0);
const list4 = list1.concat(list2, list3);
assert.equal(list1.size, 2);
assert.equal(list2.size, 5);
assert.equal(list3.size, 6);
assert.equal(list4.size, 13);
assert.equal(list4.get(0), 1);
1.在项目中引入immutable
npm install immutable
2.改造reducer:todo.js
- 原本的reducer通过es6扩展对象方式新建一个state
return { ...state, isFetching: true } -
借助immutable的api创建和修改不可变的state
import Immutable from "immutable"; const reducer = (state = Immutable.fromJS(initialState), action) => { switch (action.type) { case FETCH_TODOS_REQUEST: return state.set("isFetching", true); case FETCH_TODOS_SUCCESS: return state.merge({ isFetching: false, data: Immutable.fromJS(action.data) }); case FETCH_TODOS_FAILURE: return state.merge({ isFetching: false, error: action.error }); default: const data = state.get("data"); return state.set("data", todos(data, action)); } }; const todos = (state = Immutable.fromJS([]), action) => { switch (action.type) { case ADD_TODO: const newTodo = Immutable.fromJS({ id: action.id, text: action.text, completed: false }); return state.push(newTodo); case TOGGLE_TODO: return state.map(todo => todo.get("id") === action.id ? todo.set("completed", !todo.get("completed")) : todo ); default: return state; } };
-
Immutable.fromJS(initialState):将一个JS对象转变为一个不可变的对象
-
state.set("isFetching", true): set方法只能一次修改一个属性
-
state.merge({ isFetching: false, data: Immutable.fromJS(action.data) //将数组转化为不可变对象 })merge方法可以将一个新的JS对象merge到原有的不可变对象中
注:字符串类型本身就是一个不可变对象,不需要进行转化
-
const data = state.get("data"):get方法获取某一层级属性的值
-
const newTodo = Immutable.fromJS({ id: action.id, text: action.text, completed: false }); return state.push(newTodo);list数据解构的push区别于数组的push,会返回一个新的不可变对象
-
state.map(todo => todo.get("id") === action.id ? todo.set("completed", !todo.get("completed")) : todo );map方法针对不可变对象进行操作,遍历出来的每一个值都是不可变对象类型
访问属性仍要通过get访问,修改属性仍要通过set修改
3.改变Selector
export const getText = (state) => state.get("text")
export const getFilter = (state) => state.get("filter")
export const getVisibleTodos = (state) => {
//修改前:const {todos: {data}, filter} = state
const data = state.getIn(['todos', 'data']); //获取todos下的data属性值
const filter = state.get('filter'); //获取第一层级的filter属性值
switch (filter) {
case "all":
return data;
case "completed":
return data.filter(t => t.get('completed'));
case "active":
return data.filter(t => !t.get('completed'));
default:
return new Error("Unknown filter: " + filter);
}
};
-
state.getIn(['todos', 'data']):
getIn()API可以从外层到内层逐层的遍历不可变对象的属性
4.改变container容器型组件:todoLIstContainer.js
注意:现在获取的todos已经不是一个普通的JS对象,而是Immutable类型的不可变对象,不能直接使用,
必须转化为普通的JS对象,才能保证展示型组件可以正常使用。
const mapStateToProps = state => ({
todos: getVisibleTodos(state).toJS()
})
-
todos: getVisibleTodos(state).toJS():
toJS()将不可变对象转变为普通的JS对象
5.修改combineReducers
因为redux提供的combineReducers API只能识别普通的JS对象,而现在每个子reducer返回的state都是一个不可变类型的对象
npm install redux-immutable
import { combineReducers } from 'redux-immutable'
在高阶组件中完成Immutable不可变对象转化为JS对象
- src->HOCs->toJS.js
import React from "react"; import { Iterable } from "immutable"; export const toJS = WrappedComponent => wrappedComponentProps => { const KEY = 0; //数组的第一个位置:属性名 const VALUE = 1; //数组的第二个位置:属性值 const propsJS = Object.entries(wrappedComponentProps).reduce( (newProps, wrappedComponentProp) => { newProps[wrappedComponentProp[KEY]] = Iterable.isIterable( wrappedComponentProp[VALUE] ) ? wrappedComponentProp[VALUE].toJS() : wrappedComponentProp[VALUE]; return newProps; }, {} //初始值是默认的空对象 ); return <WrappedComponent {...propsJS} />;
-
Immutable底层存储时仍然通过数组的形式存储

-
Iterable.isIterable()判断是否是Immutable的不可变对象
资料来源:情义w的简书

资料来源:_littleTank_的简书
容器型组件中对展示型组件用高阶组件包裹
import {toJS} from "../HOCs/toJS"
export default connect(
mapStateToProps,
mapDispatchToProps
)(toJS(TodoList));
八、常用库集成:Reselect
减少state的重复计算
使用Reselect创建的selector函数,只要这个selector使用到的state没有发生改变,这个selector就不会重新去计算
https://github.com/reduxjs/reselect
- 使用Reselect
npm install reselect
- src->selectors->index.js
import { createSelector } from "reselect" export const getText = (state) => state.text export const getFilter = (state) => state.filter const getTodos = state => state.todos.data export const getVisibleTodos = createSelector( //修改前:const {todos: {data}, filter}= state [getTodos, getFilter], (todos, filter) => { switch (filter) { case "all": return todos; case "completed": return todos.filter(t => t.completed); case "active": return todos.filter(t => !t.completed); default: return new Error("Unknown filter: " + filter); } } )
注:项目来自慕课网