前言
自学react的时候看的是程墨的《深入浅出React和Redux》,这本书很适合初学者,使用create-react-app创建项目,引用一句书中原话:
用这种最简单的方式创建可运行的应用,必要的时候才会介绍底层技术拢的细节,毕竟,没有什么比一个能运行的应用更加增强开发者的信心。
过多的配置会消磨掉学习者的大部分耐心,甚至失去学习react的兴趣,尽早开始实际的react开发是最高效的学习方法。
虽然很喜欢这本书,但是本文的todo list和《深入浅出React和Redux》中的实例不同,是我自己摸索着完全自行设计开发的,一条todo记录可以有3种状态,未开始(todo),正在进行(doing)和已完成(done)。当然,如果遇到问题,这本书一直都是我的参考书。
预备知识
《深入浅出React和Redux》前4章内容,react和redux相关知识这里不再赘述,本文适合"书里说的我都懂,但是从来没有动手实践过的仔"。
项目链接
这也是注册github这么久第一个阶段性完成的项目=(:з」∠)_
欢迎加星!
https://github.com/SampleTape/todolist/
项目截图
组件划分
ToDoListPanel是最外层的容器组件,其中包含着Filter,List,AddDialog,而List中包含着多个Item。
- Filter用于通过todo的状态进行过滤,是顶部的tab选项卡。
- List就是todo列表,包含多个Item,目前作用是根据filter种类更换背景色。
- Item是一条todo,可以更换todo状态,或删除本条todo。
- AddDialog是下方新建一条todo时的对话框,只在用户点击"+"按钮时显示。
ToDoListPanel
|_Filter
|_List
|_Item
|_AddDialog
项目目录
看着这么多文件,从哪里入手刚开始的确让人头疼。
1. 首先你需要在电脑中找到一个地方,使用上文提到的命令快速创建todolist项目文件夹,其中的大部分文件,这个指令会为你自动生成:
C:\Users\zuoy\Desktop\react> creact-react-app todolist
2. 既然用到react-redux,别忘了安装它:
C:\Users\zuoy\Desktop\react\todolist> npm install react-redux
3. 然后呢,我们可以先创建几个空文件夹和空文件:
- components
- images
- styles
- actions.js
- actionTypes.js
- reducer.js
- store.js
以上就是这个项目中,我们需要集中精力的几个主要地方,已有的index.js稍后需要稍作修改,其中images中放图片,styles中放样式文件,这些我们先忽略,是不是比截图中看起来感觉轻松些(✿◡‿◡)
4. 接下来呢,从store.js开始吧,这个文件的主要作用是定义数据结构,并且通过createStore生成store,并且把reducer回调函数和初始值传给它,store中总是保存着最新鲜的数据:
import {createStore} from 'redux';
import reducer from './reducer';
const initialList = {
todos: [
{
id: 0,
what: 'dream',
status: 'todo',
}
],
filter: 'todo',
showadddialog: false,
counter: 1,
};
const store = createStore(reducer, initialList);
export default store;
todos是个数组,用来储存todo记录,每个todo记录中包含id,what,status几个字段。filter表示筛选项,showadddialog用来控制添加新todo对话框的显示隐藏,counter表示todo记录计数器,将来会被赋值给id,只增加不减少。
5. 接下来要写的时actionTypes.js,用来定义action的种类,这里需要根据你设计的功能点来确定需要几种action。
export const ADD = 'add';
export const DELETE = 'delete';
export const START = 'start';
export const FINISHED = 'finished';
export const FILTER = 'filter';
export const SHOWADDDIALOG = 'showadddialog';
6. 既然actionTypes.js 都已经写好,现在开始写actions.js吧,这里需要确定不同action种类需要传递给reducer什么样参数,所以返回值除了包含type这个字段以外,还要包含传递的参数:
import * as ActionTypes from './actionTypes';
export const add = (what) => {
return {
type: ActionTypes.ADD,
id: 0,
what,
};
};
export const deleteit = (id) => {
return {
type: ActionTypes.DELETE,
id,
};
};
export const start = (id) => {
return {
type: ActionTypes.START,
id,
};
};
export const finished = (id) => {
return {
type: ActionTypes.FINISHED,
id,
};
};
export const filter = (filter) => {
return {
type: ActionTypes.FILTER,
filter,
};
}
export const showAddDialog = (showadddialog) => {
return {
type: ActionTypes.SHOWADDDIALOG,
showadddialog,
}
}
7. 刚刚提到了reducer,现在我们开始写reducer.js,reducer负责根据action种类和参数更新state,记住不要直接修改state,而是返回一个新的对象:
import * as ActionTypes from './actionTypes';
export default (state, action) => {
let newTodos = [...state.todos];
let index = newTodos.findIndex(todo => todo.id === action.id);
switch(action.type) {
case ActionTypes.ADD:
let todo = {
id: state.counter,
what: action.what,
status: 'todo',
};
newTodos.push(todo);
return {...state, todos: newTodos, counter: state.counter + 1};
case ActionTypes.DELETE:
newTodos.splice(index,1);
return {...state, todos: newTodos};
case ActionTypes.START:
newTodos[index].status = 'doing';
return {...state, todos: newTodos};
case ActionTypes.FINISHED:
newTodos[index].status = 'done';
return {...state, todos: newTodos};
case ActionTypes.FILTER:
return {...state, filter: action.filter};
case ActionTypes.SHOWADDDIALOG:
return {...state, showadddialog: action.showadddialog};
default:
return state;
}
}
8. 基础已经打好了,可以开发组件了!
- adddialog.js
- filter.js
- item.js
- list.js
- todolistpanel.js
8.1. item.js
与其他组件一样,其中包含着一个纯函数作为傻瓜组件(展示组件)。它不需要追踪最新的state所以mapStateToProps函数只返回一个空对象;当鼠标点击相应按钮会触发一个动作,这时需要dispatch一个action,使store更新状态,需要在mapDispatchToProps函数中定义一系列函数 ,这些方法会被传递给傻瓜组件使用;connect可以根据提供的参数将傻瓜组建转换成容器组建。
import React from 'react';
import * as Actions from '../actions';
import {connect} from 'react-redux';
import start from '../images/start.png';
import finished from '../images/finished.png';
import deleteit from '../images/deleteit.png';
import '../styles/item.scss';
function Item({id, what, startToDo, finishToDo,deleteToDo}) {
return (
{what}
);
}
function mapStateToProps(state, ownProps) {
return {};
}
function mapDispatchToProps(dispatch, ownProps) {
return {
startToDo: () => {
dispatch(Actions.start(ownProps.id));
},
finishToDo: () => {
dispatch(Actions.finished(ownProps.id));
},
deleteToDo: () => {
dispatch(Actions.deleteit(ownProps.id));
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Item);
8.2. list.js
List这个组件与Item刚好相反,它仅读取state数据而不去修改它,所以mapDispatchToProps函数返回空对象,mapStateToProps函数返回对象中的字段将会传递给他的傻瓜组件。
import React from 'react';
import {connect} from 'react-redux';
import Item from './item';
import '../styles/list.scss';
function List({todos, filter}){
return (
{todos.map((todo, key) => {
if (todo.status === filter) {
return (
);
} else {
return null;
}
})}
);
}
function mapStateToProps(state) {
return {
todos: state.todos,
filter: state.filter,
};
}
function mapDispatchToProps(dispatch) {
return {};
}
export default connect(mapStateToProps,mapDispatchToProps)(List);
8.3. filter.js
Filter有点特别, 他的傻瓜组件需要给容器组建传参,从而点击不同的按钮显示不同的todo记录。这个时候傻瓜组建就没有写成纯函数的形式,而是写成了标准的React组件,并且写了handleClick这个方法,通过点击事件目标对象的id号传递不同的参数给filter这个方法。
import React from 'react';
import {connect} from 'react-redux';
import * as Actions from '../actions';
import '../styles/filter.scss';
class Filter extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick(e) {
this.props.filter(e.target.id.split('-')[1]);
}
render() {
return (
To Do
Doing
Done
);
}
}
function mapStateToProps(state, ownProps) {
return {
filterName: state.filter,
};
}
function mapDispatchToProps(dispatch) {
return {
filter: function(status) {
dispatch(Actions.filter(status));
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(Filter);
8.4. adddialog.js
AddDialog这个组件也很特别,它需要自己暂存一个state,表示用户当前输入的内容,等待用户点击"OK"这个按钮才把数据传出去。所以在这个组件的定义中,你会看到傻瓜组建也在维护自己的state。
import React from 'react';
import * as Actions from '../actions';
import {connect} from 'react-redux';
import '../styles/adddialog.scss';
class AddDialog extends React.Component {
constructor(props) {
super(props);
this.state = {
what: '',
};
this.handleAddTODO = this.handleAddTODO.bind(this);
this.handleWhatChanged = this.handleWhatChanged.bind(this);
this.handleCancel = this.handleCancel.bind(this);
}
handleAddTODO() {
this.props.addToDo(this.state.what);
this.props.showAddDialog(false);
this.setState({
what: '',
});
}
handleWhatChanged(e) {
this.setState({
what: e.target.value,
});
}
handleCancel() {
this.props.showAddDialog(false);
}
render() {
return (
Add
Cancel
OK
);
}
}
function mapStateToProps(state) {
return {
showadddialog: state.showadddialog,
};
}
function mapDispatchToProps(dispatch) {
return {
addToDo: function(what) {
dispatch(Actions.add(what));
},
showAddDialog: function(show) {
dispatch(Actions.showAddDialog(show));
}
};
}
export default connect(mapStateToProps, mapDispatchToProps)(AddDialog);
8.5. todolistpanel.js
最后把所有组件组合到一起。
import React from 'react';
import {connect} from 'react-redux';
import * as Actions from '../actions';
import List from './list';
import AddDialog from './adddialog';
import Filter from './filter';
import '../styles/todolistpanel.scss';
class ToDoListPanel extends React.Component {
constructor(props) {
super(props);
this.handleClick = this.handleClick.bind(this);
}
handleClick() {
this.props.showAddDialog(true);
}
render() {
return (
+
);
}
}
function mapStateTpProps(state) {
return {};
}
function mapDispachToProps(dispatch) {
return {
showAddDialog: function(show) {
dispatch(Actions.showAddDialog(show));
}
};
}
export default connect(mapStateTpProps, mapDispachToProps)(ToDoListPanel);
8.6 index.js
最后别忘了修改index.js,感谢Provider为我们提供的store。
import React from 'react';
import ReactDOM from 'react-dom';
import {Provider} from 'react-redux';
import ToDoListPanel from './components/todolistpanel';
import store from './store.js';
import * as serviceWorker from './serviceWorker';
import './index.css';
ReactDOM.render(
, document.getElementById('root'));
// If you want your app to work offline and load faster, you can change
// unregister() to register() below. Note this comes with some pitfalls.
// Learn more about service workers: http://bit.ly/CRA-PWA
serviceWorker.unregister();