React构建学习笔记

使用React和redux,创建一个ToDo list的demo!这依然是一个笔记!

1.开始

组织代码文件

  1. 按角色组织
1
2
3
4
5
6
7
8
reducers/
...
actions/
...
components/
...
containers/
...
  • reducer: 包含所有的redux的reducer
  • actions: 字面意思,包含所有的action构造函数
  • components: 包含所有的傻瓜组件
  • containers: 所有的容器组件
  1. 按功能组织(redux适合此类Organzied by feature)

也就是把完成同一应用功能的代码放在一个目录下.当前需要做的todo list,总共两个功能:todo listfilter list!

所以,可以如下组织代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
todoList/
actions.js
actionTypes.js
index.js
reducer.js
views/
component.js
container.js
filter/
actions.js
actionTypes.js
index.js
reducer.js
views/
component.js
container.js

大意一致,至于如何选择,建议后者.

“在最理想的情况下,应该通过增加代码就能增加系统的功能,而不是通过对现有代码的修改来增加功能”

两个知识点

低耦合性:不同功能模块之间的依赖关系应该简单而且清晰

高内聚性:一个模块应该把自己封装得足够好,让外界不要太依赖与自己内部的结构,这样就不会因为内部变化而响应外部模块的功能.

把目录看成一个模块,就应该明确这个模块的对外接口

todolist和filter目录下的index.js就是对外的接口!

如果filter中的组件想要使用todoList中的功能,应该导人todoList这个目录,因为导人一个目录的时候,默认导人的就是这个目录下的index.js文件,index.文件中导出的内容,就是这个模块想要公开出来的接口 。

确定模块边界:为了使得别的部分的代码需要导入todolist的时候,不需要依赖于todolist内部的改变而改变导入部分代码的写法!

2.设计Store状态树

状态树的原则:

  • 一个模块控制一个状态节点
  • 尽量避免冗余数据
  • 树形结构扁平

首先,Store上的state只能通过reducer修改,每一个模块都有机会导出一个自己的reducer,这个导出的reducer组多能改redux的状态树上一个节点下的数据.

也就是说,只能更新自己相关的部分模块的数据.却可以通过getState读取任意状态树上的数据!

关于保持树形结构扁平意味着尽量不要使得状态树具有很深的层次!深层次的结构经常让代码冗长.

3.TODO应用实例

TODO应用界面应该有三部分组成:

  • 代办事项列表
  • 增加待办事项的输入框和按钮
  • 待办事项过滤器

因此,可以确定分为两个功能模块:

  1. todolist(包含第一和第二部分内容)
  2. filter

思考TODO状态设计

todo状态:界面应该存在多个代表事项,并且有先后顺序.因此,可以考虑用一个数组来保存这些数据.

关于具体的代办事项,应该有很多的属性,例如内容,优先级,是否已完成等等,因此可以用object来处理.例如:

1
2
3
4
5
6
{
id: 1,
text: 喝茶,
level: 1, // 1级
completed: True // 已完成
}

关于过滤器:

  • 全部事项
  • 全部代办事项
  • 已完成事项

可以通过体现语义的常量来定义

1
2
3
const ALL = 0
const COMPLETED = 1
const UNCOMPLETED = 2

更好的方法是将数字改为具有意义的字符串,这样在以后的debug和测试中,将理解数字意义的压力.如下:

1
2
3
const ALL = 'all'
const COMPLETED = 'completed'
const UNCOMPLETED = 'uncompleted'

如此一来,简单的TODO list的状态树大概如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
todolist: [
{
text: 'first',
completed: false,
id: 0
},
{
text: 'second',
completed: false,
id: 1
},
]
filter: 'all'
}

开始编写index.js入口文件

1
2
3
4
5
6
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);

谨记在app外包上provider,其他的有app来渲染!
app如下:

1
2
3
4
5
6
7
8
9
10
class App extends Component {
render() {
return (
<div>
<Todos />
<Filter />
</div>
);
}
}

内部两个功能组件!

开始构建action

从todolist开始:

1
2
3
4
5
6
// 添加事件
export const ADD_TODO = 'TODO/ADD'
// 移除事件
export const REMOVE_TODO = 'TODO/REMOVE'
// 反转事件(从完成到待完成或者反之)
export const TOGGLE_TODO = 'TODO/TOGGLE'

为每一个action字符串前添加前缀TODO可以防止其他功能action重名问题.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 定义事件的id
let nextTodoId = 0

// 添加事件: 导出
export const addTodo = (text) => ({
type: ADD_TODO,
completed: false,
id: nextTodoId++,
text: text,
})
// 添加反转事件(传入关键字段id)
export const toggleTodo = (id) => ({
type: TOGGLE_TODO,
id: id,
})

// remove event
export const removeTodo = (id) => ({
type: REMOVE_TODO,
id: id,
})

上述代码,定义了三个基本事件,并且每个事件都返回一个数据对象,且懈怠了需要处理的关键数据.

关于reducer

为了使得整个应用只有一个reducer,则需要一些hack tips.虽然Redux的createStore只接受一个reducer,但是可以把多个功能模块下的reducer合并成一个reducer.

在Store.js中合并之:

1
2
3
4
5
6
7
import {reducer as todoReducer} from './todoList'
import {reducer as filterReducer} from './filter'

const reducer = combineReducer({
todos: todoReducer,
filter: filterReducer
})

小的reducer之间不会冲突,因此redux就可以实现多个模块的隔绝.

Todo视图

对于充当视图的react组件,往往让此组件功能精良,便于使得视图可以分布在多个文件中.

此文件为views/todo.js

1
2
3
4
5
6
7
8
export default () => {
return (
<div className="todos">
<AddTodo />
<TodoList />
</div>
)
}

将addtodo和todolist两个组件放在一个div中即可.

关于addtodo组件:

1
2
3
4
5
6
7
8
9
10
11
12
render() {
return (
<div className="add-todo">
<form onSubmit={this.onSubmit}>
<input className="new-todo" ref={this.refInput} />
<button className="add-btn" type="submit">
添加
</button>
</form>
</div>
)
}

当包含ref属性的组件完成mount过程的时候,如果ref属性是一个函数,则会调用此函数!并且,将此DOM节点当做参数穿上去.

这里调用refInput函数,其内容为:

1
2
3
refInput(node) {
this.input = node;
}

于是,node就是此input元素,可以通过DOM API访问元素内容.此组件的input属性就等于了DOM元素.通过this.input.value即可获取当前用户输入!

补充一下react表单知识

来看看submit代码:

1
2
3
4
5
6
7
8
9
10
11
12

onSubmit(ev) {
ev.preventDefault();

const input = this.input;
if (!input.value.trim()) {
return;
}

this.props.onAdd(input.value);
input.value = '';
}

在dom上,对应事件的触发调用函数的参数,是dom元素本身.

ev.preventDefault(): 阻止默认行为(form提交引发的网页跳转),如果存在新的value,则调用外部提供的onAdd函数,添加一个新的待办事项.

傻瓜组件完成,接下来就是连接傻瓜和容器组件.

1
2
3
4
5
6
7
8
9
10

const mapDispatchToProps = (dispatch) => {
return {
onAdd: (text) => {
dispatch(addTodo(text));
}
}
};

export default connect(null, mapDispatchToProps)(AddTodo);

派发Dispatch!由于没有任何Store的状态衍生的属性,因此connect函数第一参数为null.

todoList组件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20

const TodoList = ({todos, onToggleTodo, onRemoveTodo}) => {
return (
<ul className="todo-list">
{
todos.map((item) => (
<TodoItem
key={item.id}
text={item.text}
completed={item.completed}
onToggle={ () => onToggleTodo(item.id)}
onRemove={ () => onRemoveTodo(item.id)}
/>
))
// 其中,key十分重要,每一个子组件都要带有key属性,并且此key能够唯一表示此
// 子组件
}
</ul>
)
}

dispatch中两个新属性,onToggleTodo 和
onRemoveTodo 的代码遵循一样的模式,都是把接收到的参数作为参数传递给一个 action
构造函数,然后用 dispatch 方法把产生的 action 对象派发出去,这看起来是重复代码.如何优化?

1
2
3
4
5
6
7
//import {bindActionCreators} from 'redux';
/*
const mapDispatchToProps = (dispatch) => bindActionCreators({
onToggleTodo: toggleTodo,
onRemoveTodo: removeTodo
}, dispatch);
*/
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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('unsupport filter!')
}
}


const mapStateToProps = (state) => {
return {
todos: selectVisibleTodos(state.todos, state.filter)
}
}

将函数写在外面是为了简化mapStateToProps函数.

子组件TodoItem

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const TodoItem = ({onToggle, onRemove, completed, text}) => {
const checkProp = completed ? {checked: true} : {}

return (
<li className="todo-item"
style={{textDecoration: completed ? 'line-through': 'none'}}
>
<input className="toggle" type="checkbox" {...checkProp}
readOnly
onClick={onToggle}
/>

<label className="text">{text}</label>
<button className="remove" onClick={onRemove}>Del</button>
</li>
)
}

无状态组件TodoItem,吧onToggle挂在checkbox的点击事件上把onRemove挂在删除按钮的点击事件上.

Filter视图

1
2
3
4
5
6
7
8
9
const Filter = () => {
return (
<p className="filters">
<Link filter={FilterTypes.ALL}> {FilterTypes.ALL} </Link>
<Link filter={FilterTypes.COMPLETED}> {FilterTypes.COMPLETED} </Link>
<Link filter={FilterTypes.UNCOMPLETED}> {FilterTypes.UNCOMPLETED} </Link>
</p>
)
}

渲染的就是三个链接,把实际工作交给了Link组件.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const Link = ({active, children, onClick}) => {
if (active) {
return <b className="filter selected">{children}</b>;
} else {
return (
<a href="#" className="filter not-selected" onClick={(ev) => {
ev.preventDefault();
onClick();
}}>
{children}
</a>
);
}
};

作为傻瓜组件Link, 当传入属性active为true时,表示当前实例就是被选中的过滤器,不应该再次被选择!否则,渲染一个超链接.

关于children:任何一个React组件都可以访问此属性,代表的是被包裹住的子组件.

example:

1
2
3
<Foo>
<Bar>WhatEver</Bar>
</Foo>

Foo组件实例的children就是WhatEver

在render函数中吧children属性渲染出来是常用的React组件的方法.

更多详细内容略,唯一快速上手的方法就是多写代码,多看代码,理解,记录.

重点踩坑笔记

在reducer处理不同的action处,对于state的copy,应该采用JSON的方法,而非Object.assign,后者会
对state做出修改,因为多层的引用的改变,毕竟这是一个shadow copy.

使用JSON吧,deep copy.