之前用 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
应用效果图
项目运行环境:
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
生成的文件目录结构如下:
配置 package.json
打开 package.json 文件,配置好项目需要安装的依赖如下:
1 { 2 "name": "demo", 3 "version": "0.0.0", 4 "private": true, 5 "scripts": { 6 "start": "node ./bin/www" 7 }, 8 "dependencies": { 9 "body-parser": "~1.16.0", 10 "cookie-parser": "~1.4.3", 11 "debug": "~2.6.0", 12 "ejs": "~2.5.5", 13 "express": "~4.14.1", 14 "jquery": "^3.1.1", 15 "mongoose": "^4.8.6", 16 "morgan": "~1.7.0", 17 "serve-favicon": "~2.3.2" 18 }, 19 "devDependencies": { 20 "babel": "^6.23.0", 21 "babel-cli": "^6.23.0", 22 "babel-core": "^6.23.1", 23 "babel-loader": "^6.4.0", 24 "babel-preset-es2015": "^6.22.0", 25 "babel-preset-react": "^6.23.0", 26 "jquery": "^3.1.1", 27 "react": "^15.4.2", 28 "react-dom": "^15.4.2", 29 "webpack": "^2.2.1" 30 } 31 }
安装依赖:
npm install
安装 react、react-dom、webpack
npm install react react-dom webpack
Webpack 配置
在 node 项目下新建 webpack.config.js 文件,因为项目使用的技术方案为 webpack + react + es6,因此在 webpack 中配置如下:
1 var path = require("path"); 2 3 module.exports={ 4 // 项目入口 5 entry: "./src/pages/app.js", 6 // 打包文件输出路径 7 output: { 8 path: path.join(__dirname,"./public/js"), 9 filename: "bundle.js", 10 }, 11 module: { 12 loaders: [{ 13 test: /.js$/, 14 loader: "babel-loader", 15 query: { 16 presets: ['react','es2015'] 17 } 18 },{ 19 test: /.jsx$/, 20 loader: 'babel-loader', 21 query: { 22 presets: ['react', 'es2015'] 23 } 24 },{ 25 test: /.css$/, 26 loader: "style!css" 27 },{ 28 test: /.(jpg|png|otf)$/, 29 loader: "url?limit=8192" 30 },{ 31 test: /.scss$/, 32 loader: "style!css!sass" 33 }] 34 } 35 };
修改 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 文件夹所在目录,结果如下:
启动项目
npm start
打开浏览器窗口,效果如下:
那么到这里,项目基本上就跑起来了(暂时没有使用到webpack)
接下来看一下项目的目录结构:
- src 下主要存放组件文件和数据库相关文件
- public 下是静态文件和打包后的 js 文件
- router 下 index.js 定义了页面路由和封装了数据库操作的接口
- views 下 index.ejs 是项目的入口页面
- app.js 是 Node.js 服务的入口文件,在这里连接 MongoDB 数据库
- webpack.config.js 定义了项目的入口和输出文件和路径以及各种加载器 loader
首先看入口页面 index.ejs
1 <!DOCTYPE html> 2 <html> 3 <head> 4 <title><%= title %></title> 5 <link rel='stylesheet' href='/css/style.css' /> 6 </head> 7 <body> 8 9 <div id="app"> 10 11 </div> 12 13 <script src="/js/bundle.js"></script> 14 </body> 15 </html>
入口文件 src/pages/app.js
1 import React from 'react' 2 import ReactDOM from 'react-dom' 3 import Todo from './index.js' 4 5 ReactDOM.render( 6 <Todo />, 7 document.getElementById("app") 8 );
webpack会将入口文件进行合并和整理,最后输出一个bundle.js,所以所有的逻辑都在这个js文件中,因此在index.html中,只需要引入react框架和bundle.js就可以了。
数据库的定义和操作
src/schemas/todo.js
1 var mongoose = require('mongoose'); 2 var Schema = mongoose.Schema; 3 4 var Todo = new Schema({ 5 content: { 6 type: String, 7 required: true 8 }, 9 date: { 10 type: String, 11 required: true 12 } 13 }, { collection: 'todo' }); 14 15 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
1 var express = require('express'); 2 var router = express.Router(); 3 var Todo = require('../src/models/todo') 4 5 router.get('/', (req, res, next) => { 6 res.render('index', { 7 title: 'React TodoList' 8 }); 9 }); 10 11 // 获取全部的todo 12 router.get('/getAllItems', (req, res, next) => { 13 Todo.find({}).sort({'date': -1}).exec((err, todoList) => { 14 if (err) { 15 console.log(err); 16 }else { 17 res.json(todoList); 18 } 19 }) 20 }); 21 22 // 添加todo 23 router.post('/addItem', (req, res, next) => { 24 let newItem = req.body; 25 Todo.create(newItem, (err) => { 26 if (err) { 27 console.log(err); 28 }else { 29 Todo.find({}, (err, todoList) => { 30 if (err) { 31 console.log(err); 32 }else { 33 res.json(todoList); 34 } 35 }); 36 } 37 }) 38 }) 39 40 // 删除todo 41 router.post('/deleteItem', (req, res, next) => { 42 console.log(req.body); 43 let delete_date = req.body.date 44 Todo.remove({date: delete_date}, (err, result) => { 45 if (err) { 46 console.log(err) 47 }else { 48 res.json(result); 49 } 50 }); 51 }); 52 53 module.exports = router;
代码也相对简单,主要是数据的增删改查。封装好接口之后,在组件中就可以通过 ajax 进行请求来完成数据的操作。
组件分析
根据项目的功能分成了三个组件,分别是父组件 index,todo列表子组件 todo-list, todo列表子组件 todo-item。
父组件 index.js
1 import React, { Component, PropTypes } from 'react' 2 import ReactDOM from 'react-dom' 3 import $ from 'jquery' 4 import TodoList from './comps/todo-list' 5 6 class Todo extends React.Component { 7 8 constructor(props) { 9 super(props); 10 this.state = { 11 todoList: [], 12 showTooltip: false // 控制 tooltip 的显示隐藏 13 } 14 } 15 16 componentDidMount () { 17 // 获取所有的 todolist 18 this._getTodoList(); 19 } 20 21 // 获取 todolist 22 _getTodoList () { 23 const that = this; 24 $.ajax({ 25 url: '/getAllItems', 26 type: 'get', 27 dataType: 'json', 28 success: data => { 29 const todoList = that.todoSort(data) 30 that.setState({ 31 todoList 32 }); 33 }, 34 error: err => { 35 console.log(err); 36 } 37 }); 38 } 39 40 // 添加 todo 41 _onNewItem (newItem) { 42 const that = this; 43 $.ajax({ 44 url: '/addItem', 45 type: 'post', 46 dataType: 'json', 47 data: newItem, 48 success: data => { 49 const todoList = that.todoSort(data); 50 that.setState({ 51 todoList 52 }); 53 }, 54 error: err => { 55 console.log(err); 56 } 57 }) 58 } 59 60 // 删除 todo 61 _onDeleteItem (date) { 62 const that = this; 63 const postData = { 64 date: date 65 }; 66 $.ajax({ 67 url: '/deleteItem', 68 type: 'post', 69 dataType: 'json', 70 data: postData, 71 success: data => { 72 this._getTodoList(); 73 }, 74 error: err => { 75 console.log(err); 76 } 77 }) 78 } 79 80 // 对 todolist 进行逆向排序(使新录入的项目显示在列表上面) 81 todoSort (todoList) { 82 todoList.reverse(); 83 return todoList; 84 } 85 86 // 提交表单操作 87 handleSubmit(event){ 88 89 event.preventDefault(); 90 // 表单输入为空验证 91 if(this.refs.content.value == "") { 92 this.refs.content.focus(); 93 this.setState({ 94 showTooltip: true 95 }); 96 return ; 97 } 98 // 生成参数 99 var newItem={ 100 content: this.refs.content.value, 101 date: (new Date().getMonth() +1 ) + "/" 102 + new Date().getDate() + " " 103 + new Date().getHours() + ":" 104 + new Date().getMinutes() + ":" 105 + new Date().getSeconds() 106 }; 107 // 添加 todo 108 this._onNewItem(newItem) 109 // 重置表单 110 this.refs.todoForm.reset(); 111 // 隐藏提示信息 112 this.setState({ 113 showTooltip: false, 114 }); 115 } 116 117 render() { 118 return ( 119 <div className="container"> 120 <h2 className="header">Todo List</h2> 121 <form className="todoForm" ref="todoForm" onSubmit={ this.handleSubmit.bind(this) }> 122 <input ref="content" type="text" placeholder="Type content here..." className="todoContent" /> 123 { this.state.showTooltip && 124 <span className="tooltip">Content is required !</span> 125 } 126 </form> 127 <TodoList todoList={this.state.todoList} onDeleteItem={this._onDeleteItem.bind(this)} /> 128 </div> 129 ) 130 } 131 } 132 133 export default Todo;
父组件的功能:
1、在组件 DidMounted 时通过 ajax 请求所有的数据与 state 绑定实现首次渲染;
2、将数据,相应的方法分发给个子组件;
3 、实现添加、删除方法并传递给子组件。添加笔记的方法被触发的时候,发送ajax请求实现数据库数据的更新,再更新组件的state使之数据与后台数据保持一致,state一更新视图也会被重新渲染实现无刷新更新。
子组件 todo-list
1 import React from 'react'; 2 import TodoItem from './todo-item'; 3 4 class TodoList extends React.Component { 5 6 render() { 7 // 获取从父组件传递过来的 todolist 8 const todoList = this.props.todoList; 9 // 循环生成每一条 todoItem,并将 delete 方法传递给子组件 10 const todoItems = todoList.map((item,index) => { 11 return ( 12 <TodoItem 13 key={index} 14 content={item.content} 15 date={item.date} 16 onDeleteItem={this.props.onDeleteItem} 17 /> 18 ) 19 }); 20 21 return ( 22 <div> 23 { todoItems } 24 </div> 25 ) 26 } 27 } 28 29 export default TodoList;
子组件 todo-item
1 import React from 'react'; 2 3 class TodoItem extends React.Component { 4 5 constructor(props) { 6 super(props); 7 this.state = { 8 showDel: false // 控制删除 icon 的显示隐藏 9 } 10 } 11 12 handleDelete () { 13 // 获取父组件传递过来的 date 14 const date = this.props.date; 15 // 执行父组件的 delete 方法 16 this.props.onDeleteItem(date); 17 } 18 19 render() { 20 return ( 21 <div className="todoItem"> 22 <p> 23 <span className="itemCont">{ this.props.content }</span> 24 <span className="itemTime">{ this.props.date }</span> 25 <button className="delBtn" onClick={this.handleDelete.bind(this)}> 26 <img className="delIcon" src="/images/delete.png" /> 27 </button> 28 </p> 29 </div> 30 ) 31 } 32 } 33 34 export default TodoItem;
所以整个项目的组件之间的关系可以用下图表示:
可以看到,父组件中定义了所有的方法,并连同获取到得数据分发给子组件,子组件中将从父组件中获取到的数据进行处理,同时触发父组件中的方法,完成数据的操作。根据功能划分组件,逻辑是十分清晰的,这也是 React 的一大优点。
最后是相关样式文件的编写,比较简单,这里贴上代码,具体的就不分析了。
style.css
1 body { 2 padding: 50px; 3 font-size: 14px; 4 font-family: 'comic sans'; 5 color: #fff; 6 background-image: url(../images/bg2.jpg); 7 background-size: cover; 8 } 9 10 button { 11 outline: none; 12 cursor: pointer; 13 } 14 15 .container { 16 position: absolute; 17 top: 15%; 18 right: 15%; 19 width: 400px; 20 height: 475px; 21 overflow-x: hidden; 22 overflow-y: auto; 23 padding: 20px; 24 border: 1px solid #666; 25 border-radius: 5px; 26 box-shadow: 5px 5px 20px #000; 27 background: rgba(60,60,60,0.3); 28 } 29 30 .header h2 { 31 padding: 0; 32 margin: 0; 33 font-size: 25px; 34 text-align: center; 35 letter-spacing: 1px; 36 } 37 38 .todoForm { 39 margin: 20px 0 30px 0; 40 } 41 42 .todoContent { 43 display: block; 44 width: 380px; 45 padding: 10px; 46 margin-bottom: 20px; 47 border: none; 48 border-radius: 3px; 49 } 50 51 .tooltip { 52 display: inline-b lock; 53 font-size: 14px; 54 font-weight: bold; 55 color: #FF4A60; 56 } 57 58 .todoItem { 59 margin-bottom: 10px; 60 color: #333; 61 background: #fff; 62 border-radius: 3px; 63 } 64 65 .todoItem p { 66 position: relative; 67 padding: 8px 10px; 68 font-size: 12px; 69 } 70 71 .itemTime { 72 position: absolute; 73 right: 40px; 74 } 75 76 .delBtn { 77 display: none; 78 position: absolute; 79 right: 3px; 80 bottom: 2px; 81 background: #fff; 82 border: none; 83 cursor: pointer; 84 } 85 86 .todoItem p:hover .delBtn { 87 display: block; 88 } 89 90 .delBtn img { 91 height: 20px; 92 }
最后使用 webpack 进行打包,启动项目,就可以在浏览器中看到效果了。最后附上一张控制台的图片。