Node.js + React + MongoDB 实现 TodoList 单页应用

时间:2024-05-04 08:04:49

之前用 Ant Design 开发了一个项目,因此对 React 的特性有了一定的了解,React 使用封装组件的思想,组件各自维护自己的状态和 UI, 组件之间通过 props 传递数据和方法。当状态更新时自动重绘整个组件,从而达到局部刷新的效果,大大提高了 DOM 更新的效率,同时组件化十分有利于维护。在对 React 进行进一步的学习后,使用 Node.js + React 的方式实现了一个简单的 TodoList 单页应用,同时涉及简单的 MongoDB 数据库操作,总的来说,项目相对简单,十分适合 React 的入门学习。

Github地址: https://github.com/wx1993/Node-React-MongoDB-TodoList

应用功能

1、添加 todoList

2、删除 todoList

应用效果图

Node.js + React + MongoDB 实现 TodoList 单页应用

项目运行环境:

Windows/Mac

Node.js v6.9.4 or later

MongoDB

安装和配置 MongoDB: 

Mac:http://www.cnblogs.com/wx1993/p/5187530.html

Windows: http://www.cnblogs.com/wx1993/p/5206587.html

        http://www.cnblogs.com/wx1993/p/6518248.html

项目初始化

创建node项目(已经安装 Node.js, express,express-generator)

express -e demo

生成的文件目录结构如下:

Node.js + React + MongoDB 实现 TodoList 单页应用

配置 package.json

打开 package.json 文件,配置好项目需要安装的依赖如下:

 {
"name": "demo",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"body-parser": "~1.16.0",
"cookie-parser": "~1.4.3",
"debug": "~2.6.0",
"ejs": "~2.5.5",
"express": "~4.14.1",
"jquery": "^3.1.1",
"mongoose": "^4.8.6",
"morgan": "~1.7.0",
"serve-favicon": "~2.3.2"
},
"devDependencies": {
"babel": "^6.23.0",
"babel-cli": "^6.23.0",
"babel-core": "^6.23.1",
"babel-loader": "^6.4.0",
"babel-preset-es2015": "^6.22.0",
"babel-preset-react": "^6.23.0",
"jquery": "^3.1.1",
"react": "^15.4.2",
"react-dom": "^15.4.2",
"webpack": "^2.2.1"
}
}

安装依赖:

npm install

安装 react、react-dom、webpack

npm install react react-dom webpack

Webpack 配置

在 node 项目下新建 webpack.config.js 文件,因为项目使用的技术方案为 webpack + react + es6,因此在 webpack 中配置如下:

 var path = require("path");

 module.exports={
// 项目入口
entry: "./src/pages/app.js",
// 打包文件输出路径
output: {
path: path.join(__dirname,"./public/js"),
filename: "bundle.js",
},
module: {
loaders: [{
test: /\.js$/,
loader: "babel-loader",
query: {
presets: ['react','es2015']
}
},{
test: /\.jsx$/,
loader: 'babel-loader',
query: {
presets: ['react', 'es2015']
}
},{
test: /\.css$/,
loader: "style!css"
},{
test: /\.(jpg|png|otf)$/,
loader: "url?limit=8192"
},{
test: /\.scss$/,
loader: "style!css!sass"
}]
}
};

修改 app.js,连接数据库

打开项目中的 app.js 文件,添加代码:

var mongoose = require('mongoose')
mongoose.connect('mongodb://localhost:27017/todo')

使用 node.js 的 mongoose 库方法连接 MongoDB 数据库, 27017 是数据库默认端口号,todo是数据库名称,可自定义。

启动 MongoDB 服务

在命令行窗口输入命令 

mongod --dbpath D:mongodb/data

dbpath 后面的是 MongoDB 下 data 文件夹所在目录,结果如下:

Node.js + React + MongoDB 实现 TodoList 单页应用

启动项目

npm start

打开浏览器窗口,效果如下:

Node.js + React + MongoDB 实现 TodoList 单页应用

那么到这里,项目基本上就跑起来了(暂时没有使用到webpack)

接下来看一下项目的目录结构:

Node.js + React + MongoDB 实现 TodoList 单页应用

  • src 下主要存放组件文件和数据库相关文件
  • public 下是静态文件和打包后的 js 文件
  • router 下 index.js 定义了页面路由和封装了数据库操作的接口
  • views 下 index.ejs 是项目的入口页面
  • app.js 是 Node.js 服务的入口文件,在这里连接 MongoDB 数据库
  • webpack.config.js 定义了项目的入口和输出文件和路径以及各种加载器 loader  

首先看入口页面 index.ejs

 <!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/css/style.css' />
</head>
<body> <div id="app"> </div> <script src="/js/bundle.js"></script>
</body>
</html>

入口文件 src/pages/app.js

 import React from 'react'
import ReactDOM from 'react-dom'
import Todo from './index.js' ReactDOM.render(
<Todo />,
document.getElementById("app")
);

webpack会将入口文件进行合并和整理,最后输出一个bundle.js,所以所有的逻辑都在这个js文件中,因此在index.html中,只需要引入react框架和bundle.js就可以了。

数据库的定义和操作

src/schemas/todo.js

 var mongoose = require('mongoose');
var Schema = mongoose.Schema; var Todo = new Schema({
content: {
type: String,
required: true
},
date: {
type: String,
required: true
}
}, { collection: 'todo' }); module.exports = Todo;

数据集合十分简单,两个字段,内容和时间,并保存在 todo 表中,然后在 model 下的 todo.js 中定义数据库模型:

var mongoose = require('mongoose');
var TodoSchema = require('../schemas/todo');
var TodoBox = mongoose.model('TodoBox', TodoSchema); module.exports = TodoBox;

在路由中封装数据库操作接口,如下:

routes/index.js

 var express = require('express');
var router = express.Router();
var Todo = require('../src/models/todo') router.get('/', (req, res, next) => {
res.render('index', {
title: 'React TodoList'
});
}); // 获取全部的todo
router.get('/getAllItems', (req, res, next) => {
Todo.find({}).sort({'date': -1}).exec((err, todoList) => {
if (err) {
console.log(err);
}else {
res.json(todoList);
}
})
}); // 添加todo
router.post('/addItem', (req, res, next) => {
let newItem = req.body;
Todo.create(newItem, (err) => {
if (err) {
console.log(err);
}else {
Todo.find({}, (err, todoList) => {
if (err) {
console.log(err);
}else {
res.json(todoList);
}
});
}
})
}) // 删除todo
router.post('/deleteItem', (req, res, next) => {
console.log(req.body);
let delete_date = req.body.date
Todo.remove({date: delete_date}, (err, result) => {
if (err) {
console.log(err)
}else {
res.json(result);
}
});
}); module.exports = router;

代码也相对简单,主要是数据的增删改查。封装好接口之后,在组件中就可以通过 ajax 进行请求来完成数据的操作。

组件分析

根据项目的功能分成了三个组件,分别是父组件 index,todo列表子组件 todo-list, todo列表子组件 todo-item。

父组件 index.js

 import React, { Component, PropTypes } from 'react'
import ReactDOM from 'react-dom'
import $ from 'jquery'
import TodoList from './comps/todo-list' class Todo extends React.Component { constructor(props) {
super(props);
this.state = {
todoList: [],
showTooltip: false // 控制 tooltip 的显示隐藏
}
} componentDidMount () {
// 获取所有的 todolist
this._getTodoList();
} // 获取 todolist
_getTodoList () {
const that = this;
$.ajax({
url: '/getAllItems',
type: 'get',
dataType: 'json',
success: data => {
const todoList = that.todoSort(data)
that.setState({
todoList
});
},
error: err => {
console.log(err);
}
});
} // 添加 todo
_onNewItem (newItem) {
const that = this;
$.ajax({
url: '/addItem',
type: 'post',
dataType: 'json',
data: newItem,
success: data => {
const todoList = that.todoSort(data);
that.setState({
todoList
});
},
error: err => {
console.log(err);
}
})
} // 删除 todo
_onDeleteItem (date) {
const that = this;
const postData = {
date: date
};
$.ajax({
url: '/deleteItem',
type: 'post',
dataType: 'json',
data: postData,
success: data => {
this._getTodoList();
},
error: err => {
console.log(err);
}
})
} // 对 todolist 进行逆向排序(使新录入的项目显示在列表上面)
todoSort (todoList) {
todoList.reverse();
return todoList;
} // 提交表单操作
handleSubmit(event){ event.preventDefault();
// 表单输入为空验证
if(this.refs.content.value == "") {
this.refs.content.focus();
this.setState({
showTooltip: true
});
return ;
}
// 生成参数
var newItem={
content: this.refs.content.value,
date: (new Date().getMonth() +1 ) + "/"
+ new Date().getDate() + " "
+ new Date().getHours() + ":"
+ new Date().getMinutes() + ":"
+ new Date().getSeconds()
};
// 添加 todo
this._onNewItem(newItem)
// 重置表单
this.refs.todoForm.reset();
// 隐藏提示信息
this.setState({
showTooltip: false,
});
} render() {
return (
<div className="container">
<h2 className="header">Todo List</h2>
<form className="todoForm" ref="todoForm" onSubmit={ this.handleSubmit.bind(this) }>
<input ref="content" type="text" placeholder="Type content here..." className="todoContent" />
{ this.state.showTooltip &&
<span className="tooltip">Content is required !</span>
}
</form>
<TodoList todoList={this.state.todoList} onDeleteItem={this._onDeleteItem.bind(this)} />
</div>
)
}
} export default Todo;

父组件的功能:

1、在组件 DidMounted 时通过 ajax 请求所有的数据与 state 绑定实现首次渲染;

2、将数据,相应的方法分发给个子组件;

3 、实现添加、删除方法并传递给子组件。添加笔记的方法被触发的时候,发送ajax请求实现数据库数据的更新,再更新组件的state使之数据与后台数据保持一致,state一更新视图也会被重新渲染实现无刷新更新。

子组件 todo-list

 import React from 'react';
import TodoItem from './todo-item'; class TodoList extends React.Component { render() {
// 获取从父组件传递过来的 todolist
const todoList = this.props.todoList;
// 循环生成每一条 todoItem,并将 delete 方法传递给子组件
const todoItems = todoList.map((item,index) => {
return (
<TodoItem
key={index}
content={item.content}
date={item.date}
onDeleteItem={this.props.onDeleteItem}
/>
)
}); return (
<div>
{ todoItems }
</div>
)
}
} export default TodoList;

子组件 todo-item

 import React from 'react';

 class TodoItem extends React.Component {

     constructor(props) {
super(props);
this.state = {
showDel: false // 控制删除 icon 的显示隐藏
}
} handleDelete () {
// 获取父组件传递过来的 date
const date = this.props.date;
// 执行父组件的 delete 方法
this.props.onDeleteItem(date);
} render() {
return (
<div className="todoItem">
<p>
<span className="itemCont">{ this.props.content }</span>
<span className="itemTime">{ this.props.date }</span>
<button className="delBtn" onClick={this.handleDelete.bind(this)}>
<img className="delIcon" src="/images/delete.png" />
</button>
</p>
</div>
)
}
} export default TodoItem;

所以整个项目的组件之间的关系可以用下图表示:

Node.js + React + MongoDB 实现 TodoList 单页应用

可以看到,父组件中定义了所有的方法,并连同获取到得数据分发给子组件,子组件中将从父组件中获取到的数据进行处理,同时触发父组件中的方法,完成数据的操作。根据功能划分组件,逻辑是十分清晰的,这也是 React 的一大优点。

最后是相关样式文件的编写,比较简单,这里贴上代码,具体的就不分析了。

style.css

 body {
padding: 50px;
font-size: 14px;
font-family: 'comic sans';
color: #fff;
background-image: url(../images/bg2.jpg);
background-size: cover;
} button {
outline: none;
cursor: pointer;
} .container {
position: absolute;
top: 15%;
right: 15%;
width: 400px;
height: 475px;
overflow-x: hidden;
overflow-y: auto;
padding: 20px;
border: 1px solid #666;
border-radius: 5px;
box-shadow: 5px 5px 20px #000;
background: rgba(60,60,60,0.3);
} .header h2 {
padding:;
margin:;
font-size: 25px;
text-align: center;
letter-spacing: 1px;
} .todoForm {
margin: 20px 0 30px 0;
} .todoContent {
display: block;
width: 380px;
padding: 10px;
margin-bottom: 20px;
border: none;
border-radius: 3px;
} .tooltip {
display: inline-b lock;
font-size: 14px;
font-weight: bold;
color: #FF4A60;
} .todoItem {
margin-bottom: 10px;
color: #333;
background: #fff;
border-radius: 3px;
} .todoItem p {
position: relative;
padding: 8px 10px;
font-size: 12px;
} .itemTime {
position: absolute;
right: 40px;
} .delBtn {
display: none;
position: absolute;
right: 3px;
bottom: 2px;
background: #fff;
border: none;
cursor: pointer;
} .todoItem p:hover .delBtn {
display: block;
} .delBtn img {
height: 20px;
}

最后使用 webpack 进行打包,启动项目,就可以在浏览器中看到效果了。最后附上一张控制台的图片。

Node.js + React + MongoDB 实现 TodoList 单页应用