Node.js + React + MongoDB 實現 TodoList 單頁應用

以前用 Ant Design 開發了一個項目,所以對 React 的特性有了必定的瞭解,React 使用封裝組件的思想,組件各自維護本身的狀態和 UI, 組件之間經過 props 傳遞數據和方法。當狀態更新時自動重繪整個組件,從而達到局部刷新的效果,大大提升了 DOM 更新的效率,同時組件化十分有利於維護。在對 React 進行進一步的學習後,使用 Node.js + React 的方式實現了一個簡單的 TodoList 單頁應用,同時涉及簡單的 MongoDB 數據庫操做,總的來講,項目相對簡單,十分適合 React 的入門學習。css

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

應用功能node

一、添加 todoListreact

二、刪除 todoListjquery

應用效果圖webpack

 

項目運行環境:git

Windows/Maces6

Node.js v6.9.4 or latergithub

MongoDBweb

 

安裝和配置 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;

父組件的功能:

一、在組件 DidMounted 時經過 ajax 請求全部的數據與 state 綁定實現首次渲染;

二、將數據,相應的方法分發給個子組件;

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 進行打包,啓動項目,就能夠在瀏覽器中看到效果了。最後附上一張控制檯的圖片。

相關文章
相關標籤/搜索