我们已经介绍了React 的基本工作方式,也知道了 Redux 在组合React 组件中的作用,但是更多的只是了解其基本原理和使用方法 。
但是接下来我们将要了解为以下几个方面
1) 模块化应用的要点
2) 代码文件的组织方式
3) 状态树的设计
4) 开发辅助工具
简单的看,React和Redux都执行一个公式
UI=render(state)
来产生用户界面,但是架构一个新应用,我们必须考虑以下几点
1) 码文件的组织结构;
2) 确定模块的边界;
3) Store 的状态树设计。
最后我们会动手写一个TODO应用,来学习各个层次的知识。
(1)接角色组织
目前包含Redux的github和网上大多数教程都采用这种方式。
reducer 目录包含所有 Redux 的 reducer;
actions 目录包含所有 action 构造函数;
components 目录包含所有的傻瓜组件;
views目录包含所有的容器组件 。
当然我们上家也是这样基于角色组织,但是项目大后。就发现一个问题,当我们修改views内部容器组件,就需要同时修改actions和actionTypes和 Reducers 内相对应的文件。这无疑让人头疼。
(2)接功能组织
Redux 应用适合于“按功能组织”( Organzied by Feature ),也就是把完成同一应用功能的代码放在一个目录下,一个应用功能包含多个角色的代码 。
在 Redux 中,不同的角色就是 reducer 、 actions 和视图 而应用功能对应的就是用户界面上的交互模块。
我们以 Todo 应用为例子,这个应用的两个基本功能就是 TodoList 和 Filter ,所以代码就这样组织,文件目录列表如下:
这里,我将原来角色布局顶层的views更改为pages。Todo内部有两个功能模块。
每个基本功能对应的其实就是一个功能模块,每个功能模块对应一个目录,这个例子中分别是 todoList 和 filter ,每个目录下包含同样名字的角色文件:
actionTypes.js 定义 action 类型;
actions.js 定义 action 构造函数,决定了这个功能模块可以接受的动作;
reducer.js 定义这个功能模块如何相应 actions. 中定义的动作;
views 目录,包含这个功能模块中所有的 React 组件,包括傻瓜组件和容器组件;
index.js 这个文件把所有的角色导入,然后统一导出 。
在这种组织方式下,当你要修改某个功能模块的代码的时候,只要关注对应的目录就行了,所有需要修改的代码文件都在能这个目录下找到 。
在最理想的情况下,我们应该通过增加代码就能增加系统的功能,而不是通过对现有代码的修改来增加功能 。
一一-Robert C. Martin
不同功能模块之间的依赖关系应该简单而且清晰,也就是所谓的保持模块之间低耦合性;
一个模块应该把自己的功能封装得很好,让外界不要太依赖与自己内部的结构,这样不会因为内部的变化而影响外部模块的功能,这就是所谓高内聚性。
整个 Redux 应用而言,整体由模块构成,但是模块不再是 React 组件,而是由 React组件加上相关 reducer 和 actions 构成的一个小整体 。
以我们将要实现实现的 Todo 应用为例,功能模块就是 todoList 和 filter,这两个功能模块分别用各自的 React 组件、 reducer 和 action 定义 。然后,通过 index.js 文件,这个文件就是
我们的模块接口 。
状态树设计要遵循如下几个原则:
一个模块控制一个状态节点
避免冗余数据
树形结构扁平
Redux Store上的全部状态,在任何时候,对任何模块都是开放的,通过 store.getState()总能够读取当整个状态树的数据,但是只能更新自己相关那一部分模块的数据 。
冗余数据是一致性的大敌,如果在 Store 上存储冗余数据,那么维持不同部分数据一致就是一个大问题
如果树形结构层次很深,往往意味着树形很复杂,一个很复杂的状态树是难以管理
相关代码已上传github。
todo-feature分支
首先, 从上面的项目结构可以看出,我们将Todo页面分为Filter,和TodoList模块。那么Todo应用Store的设计如下
上面图,todos是个list数组,当每新增一个元素,都要标记当前事项completed字符为‘已完成’或者‘未完成’。而那些事项显示,则有todos和filter共同决定。
顶层TodoList/index.js 负责组装
import React from 'react';
import {view as Todos} from './TodoList';
import {view as Filter} from './Filter';
function Todo() {
return (
);
}
export default Todo;
在views/addTodo.js 中
onSubmit(ev) {
ev.preventDefault();
const input = this.input;
if (!input.value.trim()) {
return;
}
this.props.onAdd(input.value);
input.value = '';
}
refInput(node) {
this.input = node;
}
render() {
return (
"add-todo">
)
}
当一个包含 ref属性的组件完成装载( mount)过程的时候,会看一看 ref属性是不是一个函数,如果是,就会调用这个函数,参数就是这个组件代表的 DOM 元素 。
当 reflnput 被调用时,参数 node 就是那个 input 元素, refinput 把这个 node 记录在成员变量 input 中 。
于是,当组件完成 mount 之后 ,就可以通过 this.input 来访问那个 input 元素。这是一个 DOM 元素,可以使用任何 DOMAPI 访问元素内容,通过访问 this.input.value 就可以直接读取当前的用户输入 。
todoList.js
首先来看mapStateToProps方法。
const selectVisibleTodos = (todos, filter) => {
switch (filter) {
case FilterTypes.ALL:
return todos;
case FilterTypes.COMPLETED:
return todos.filter(item => item.completed);
case FilterTypes.UNCOMPLETED:
return todos.filter(item => !item.completed);
default:
throw new Error('unsupported filter');
}
}
const mapStateToProps = (state) => ({
todos: selectVisibleTodos(state.todos, state.filter)
})
Store 上的 filter 状态决定 to dos 状态上取哪些元素来显示,这个过程涉及对 filter 的 switch 判断。
首先来看mapDispatchToProps 方法。
const mapDispatchToProps = (dispatch) => {
return {
onToggleTodo: (id) => {
dispatch(toggleTodo(id));
},
onRemoveTodo: (id) => {
dispatch(removeTodo(id));
}
};
};
mapDispatchToProps 函数产生的两个新属性 onToggleTodo 和onRemoveTodo 的代码遵循一样的模式,都是把接收到的参数作为参数传递给一个 action构造函数,然后用 dispatch 方法把产生的 action 对象派发出去,这看起来是重复代码 。
实际上。 Redux 已经提供了一个 bindActionCreators 方法来消除这样的重复代码,显而易见很多 mapDispatchToProps 要做的事情只是把 action 构造函数和 prop 关联起来,所以直接以 prop 名为字段名,以 action 构造函数为对应字段值,把这样的对象传递给bindActionCreators 就可以
const mapDispatchToProps = (dispatch) => bindActionCreators({
onToggleTodo: toggleTodo,
onRemoveTodo: removeTodo
}, dispatch);
更进一步,可以直接让 mapDispatchToProps 是一个 prop 到 action 构造函数的映射,
这样连 bindActionCreators 函数都不用
const mapDispatchToProps = ({
onToggleTodo: toggleTodo,
onRemoveTodo: removeTodo
})
最后:
后面代码太多,不想继续写。可以查看git分支查看代码