0%

React(二)

React Todos

了解了React基础之后,得做个应用,首选Todos。做一个这样的Todos:最终效果图

新建工程

执行create-react-app react_todos,新建一个工程,然后安装包:npm install express body-parser ejs mongojs material-ui axios --save

配置服务器和数据库

在工作目录下新建server.js:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
const express = require('express');
const app = express();
const PORT = process.env.PORT || 8080;
const path = require('path');
const bodyParser = require('body-parser');
// const cors = require('cors');
const ejs = require('ejs');

// 引入路由
const index = require('./routes/index');
const todos = require('./routes/todos');

// 渲染引擎
app.set('view engine', 'ejs');
app.engine('html', ejs.renderFile);

// 跨域请求处理
// app.use(cors());

// 配置静态文件
app.use(express.static(path.join(__dirname, 'build')));

//配置body-parser用以解析请求
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({extended: false}));

// 配置路由
app.use('/api', todos);
app.use('/', index);

// 启动服务并监听8080端口
app.listen(PORT, () => {
console.log(`Server started on port ${PORT}`);
});

server使用两个路由,目录下新建routes文件夹,新建两个文件:
routes/index.js

1
2
3
4
5
6
7
8
9
const express = require('express');
const router = express.Router();

// 用来Serve应用的index.html
router.get('/', (req, res) => {
res.render(index.html)
});

module.exports = router;

routes/todos.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
const express = require('express');
const router = express.Router();

// 连接数据库,使用mlab
const mongojs = require('mongojs');
const db = mongojs('mongodb://admin:admin@ds048319.mlab.com:48319/todos', ['todos'])

// 获取所有todo
router.get('/todos', (req, res, next) => {
db.todos.find((err, todos) => {
if (err) {
res.send(err);
} else {
res.json(todos);
}
});
});

// 获取一个todo
router.get('/todo/:id', (req, res, next) => {
db.todos.findOne({
_id: mongojs.ObjectId(req.params.id)
}, (err, todo) => {
if (err) {
res.send(err);
} else {
res.json(todo);
}
});
});

// 新建一个todo
router.post('/todos', (req, res, next) => {
const todo = req.body;
if (!todo.text || !(todo.isCompleted + '')) {
res.status(400);
res.json({
"error": "Invalid Data"
});
} else {
db.todos.save(todo, (err, result) => {
if (err) {
res.send(err);
} else {
res.json(result);
}
});
}
});

// 更新todo
router.put('/todo/:id', (req, res, next) => {
const todo = req.body;
const updObj = {};

if (todo.isCompleted) {
updObj.isCompleted = todo.isCompleted;
}

if (todo.text) {
updObj.text = todo.text;
}

if (!updObj) {
res.status(400);
res.json({
"error": "invalid Data"
});
} else {
db.todos.update({
_id: mongojs.ObjectId(req.params.id)
}, updObj, {}, (err, result) => {
if (err) {
res.send(err);
} else {
res.json(result);
}
});
}
});

// 删除todo
router.delete('/todo/:id', (req, res, next) => {
db.todos.remove({
_id: mongojs.ObjectId(req.params.id)
}, '', (err, result) => {
if (err) {
res.send(err);
} else {
res.json(result);
}
});
});

module.exports = router;

应用节点容器

app.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
import React from 'react';

// 使用axios做数据请求
import axios from 'axios';
const CancelToken = axios.CancelToken;
let cancel;

// 使用Material-ui
import injectTapEventPlugin from 'react-tap-event-plugin';
injectTapEventPlugin();
import MuiThemeProvider from 'material-ui/styles/MuiThemeProvider';
import AppBar from 'material-ui/AppBar';
import Paper from 'material-ui/Paper';
import FloatingActionButton from 'material-ui/FloatingActionButton';
import ContentAdd from 'material-ui/svg-icons/content/add';
import CircularProgress from 'material-ui/CircularProgress';
import {BottomNavigation, BottomNavigationItem} from 'material-ui/BottomNavigation';
import IconLocationOn from 'material-ui/svg-icons/communication/location-on';
const nearbyIcon = <IconLocationOn />;

// 引入组件
import TodoList from './components/TodoList';
import AddTodo from './components/AddTodo';

// 样式
const outerPaper = {
minHeight: 600,
width: 360,
margin: '20px auto 40px',
textAlign: 'center'
};

const innerPaper = {
minHeight: 450,
width: '90%',
margin: '20px auto'
};

class App extends React.Component {
constructor() {
super();
this.state = {
todos: [],
showAdd: false,
selectedIndex: 1
}

this.fetchData = this.fetchData.bind(this);
this.handleToggle = this.handleToggle.bind(this);
this.addTodo = this.addTodo.bind(this);
this.deleteTodo = this.deleteTodo.bind(this);
this.toggleAdd = this.toggleAdd.bind(this);
this.selectFilter = this.selectFilter.bind(this);
this.selectTodos = this.selectTodos.bind(this);
}

// 加载组件时从服务器获取一次数据
componentDidMount() {
this.fetchData();
}

// 卸载组件时取消请求
componentWillUnmount() {
cancel();
}

// 获取所有的todos
fetchData() {
axios.get('/api/todos', {
cancelToken: new CancelToken(c => cancel = c)
})
.then(result => {
this.setState({
todos: result.data
});
})
.catch(err => {
console.log(err);
});
}

// 选择显示方式
selectFilter(index) {
this.setState({selectedIndex: index});
}

// 根据选择的显示方式来确定最终出现在页面上的内容
selectTodos(todos, index) {
switch (index) {
case 0:
return todos;
case 1:
return todos.filter(todo => !todo.isCompleted);
case 2:
return todos.filter(todo => todo.isCompleted);
}
}

// 隐藏/显示Add组件
toggleAdd() {
this.setState({
showAdd: !this.state.showAdd
});
}

// 新建一个todo,然后重新获取数据并隐藏Add表单
addTodo(text) {
if (text) {
axios.post('/api/todos', {
text,
isCompleted: false
})
.then(result => {
this.fetchData();
this.toggleAdd();
})
.catch(err => {
console.log(err);
});
}
}

// 处理todo的完成/未完成,然后获取数据
handleToggle(id) {
const url = `/api/todo/${id}`;
let currentTodo;
this.state.todos.forEach(todo => {
if (todo._id === id) {
currentTodo = todo;
};
});
axios.put(url, {
text: currentTodo.text,
isCompleted: !currentTodo.isCompleted
})
.then(result => {
this.fetchData();
})
.catch(err => console.log(err));
}

// 删除一个todo,然后获取数据
deleteTodo(id) {
axios.delete(`/api/todo/${id}`)
.then(result => {
this.fetchData();
})
.catch(err => console.log(err));
}

render() {
return (
<MuiThemeProvider>
<div>
<AppBar title="React Todos" showMenuIconButton={false} />
<Paper style={outerPaper}>
// 根据showAdd的状态确定是显示AddTodo表单还是FloatingActionButton按钮
{this.state.showAdd ?
<AddTodo addTodo={this.addTodo} toggleAdd={this.toggleAdd} />
:
<FloatingActionButton
onClick={this.toggleAdd}
style={{marginTop: 20}}
>
<ContentAdd />
</FloatingActionButton>
}
<Paper style={innerPaper} zDepth={5}>
// 根据todos.length确定数据是否加载完毕,如果没有就显示个转圈,如果有显示列表
{this.state.todos.length ? '' : <CircularProgress size={60} thickness={5} style={{marginTop: 20}}/>}
// 选择要显示的内容
<BottomNavigation selectedIndex={this.state.selectedIndex}>
<BottomNavigationItem
label="All"
icon={nearbyIcon}
onTouchTap={() => this.selectFilter(0)}
/>
<BottomNavigationItem
label="Todo"
icon={nearbyIcon}
onTouchTap={() => this.selectFilter(1)}
/>
<BottomNavigationItem
label="Done"
icon={nearbyIcon}
onTouchTap={() => this.selectFilter(2)}
/>
</BottomNavigation>
// 根据selectedIndex,确定传递给TodoList组件的数据
<TodoList
todos={this.selectTodos(this.state.todos, this.state.selectedIndex)}
toggleTodo={this.handleToggle}
deleteTodo={this.deleteTodo}
/>
</Paper>
</Paper>
</div>
</MuiThemeProvider>
)
}
}
export default App;

components/TodoList.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
import React from 'react';

import {List, ListItem} from 'material-ui/List';
import Checkbox from 'material-ui/Checkbox';
import RaisedButton from 'material-ui/RaisedButton';

const TodoList = ({todos, toggleTodo, deleteTodo}) => {
return (
<div>
<List>
{ todos.map(todo =>
<ListItem key={todo._id}>
<Checkbox
style={{display: "inline-block"}}
label={todo.text || ''}
labelStyle={{fontSize: "1.5em"}}
checked={todo.isCompleted || false}
onCheck={() => toggleTodo(todo._id)}
/>
<RaisedButton label="delete"
style={{margin: 12}}
secondary={true}
onClick={() => deleteTodo(todo._id)}
/>
</ListItem>)
}
</List>
</div>
);
};

export default TodoList;

components/addTodo.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
import React from 'react';

import TextField from 'material-ui/TextField';
import RaisedButton from 'material-ui/RaisedButton';
import {blue500} from 'material-ui/styles/colors';

let inputText = null;

const styles = {
underlineStyle: {
borderColor: blue500
},
floatingLabelStyle: {
color: blue500,
}
};

const AddTodo = ({addTodo, toggleAdd}) => {
return (
<div>
<TextField
underlineStyle={styles.underlineStyle}
hintText="Enter Todo Here"
floatingLabelText="New Todo"
onChange={e => inputText = e.target.value}
required
/>
<RaisedButton
label="Add"
primary={true}
style={{margin: 12}}
onClick={() => addTodo(inputText)}
/>
<RaisedButton
label="Cancel"
secondary={true}
style={{margin: 12}}
onClick={() => {
toggleAdd();
}}
/>
</div>
);
};

export default AddTodo;

怎么样,是不是很简单呢?[微笑]