【重点突破】—— Redux基础&进阶:用好Redux必备(二)
前言:正在学习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); } } )
注:项目来自慕课网