詳解 Node + Redux + MongoDB 實現 Todolist

前言

爲何要使用 Redux?

組件化的開發思想解放了繁瑣低效的 DOM 操做,以 React 來講,一切皆爲狀態,經過狀態能夠控制視圖的變化,而後隨着應用項目的規模的不斷擴大和應用功能的不斷豐富,過多的狀態變得難以控制,以致於當不一樣的組件中觸發了同一個狀態的修改或者引起了視圖的更新,咱們可能搞不清楚到底發生了什麼,state 的變化已經變得有些難以預測和不受控制,所以 Redux 應運而生,經過對 Flux 思想的實踐和加強,對狀態更新發生的時間和方式進行限制,Redux 試圖讓 state 的變化變得可預測。html

項目簡介

在學了一段時間 Redux 以後,開始嘗試對以前作過的 Todolist 單頁應用進行重構,須要說明的是,由於應用自己很是迷你,因此可能沒法明顯地體現使用 Redux 的優點,可是基本上可以比較清晰得說明 Redux 的工做流程,相信各位在閱讀了下面對項目實用 Redux 重構過程的分析後,會有很大的收穫和體會。node

技術棧:  Node.js  React  Redux Webpack MongoDBreact

項目源代碼的 Github 地址:https://github.com/wx1993/Node-Redux-MongoDB-TodoListjquery

項目的搭建和環境的配置,可參考上一篇博客: Node.js + React + MongoDB 實現 TodoList 單頁應用webpack

相關的操做和配置能夠參考博客:git

Node 項目的建立:http://www.cnblogs.com/wx1993/p/5765301.htmles6

MongoDB 的安裝和配置:http://www.cnblogs.com/wx1993/p/5187530.html (Mac) github

             http://www.cnblogs.com/wx1993/p/5206587.html(windows)web

Git 入門和經常使用命令詳解:http://www.cnblogs.com/wx1993/p/6230435.htmlajax

參考資料

在學習的過程當中,主要受了如下資料和博客的啓發:

《深刻 React 技術棧》 第五章 <深刻 Redux 應用架構>  

Redux 中文文檔

redux —— 入門實例 TodoList

Redux狀態管理方法與實例

如何通俗易懂地理解 Redux?

Redux 基礎

Redux 的三大原則

1. 單一數據源。

應用只有惟一的數據源,整個應用的狀態都保存在一個對象中,爲提取出整個應用的狀態進行持久化提供可能,同時 Redux 提供的 combineReducers 方法對數據源過於龐大的問題進行了有效的化解。

2. 狀態是隻讀的。

在 Redux 中,沒法直接經過 setState() 來修改狀態,而是經過定義 reducer ,根據當前觸發的 action 類型對當前的 state 進行迭代。reducer(previousState, action) => newState

3. 狀態修改由純函數完成。

狀態修改經過 reducer 來實現,每個 reducer 都是純函數,當接受必定的 state 和 action,返回的 newState 都是固定不變的。

Redux 組成部分

1. store:createStore(reducer,initialState)方法生成,用於維護整個應用的 state。store 包含如下四個方法:

  • getState():獲取 store 中當前的狀態
  • dispatch(action):分發 action,更新 state
  • subscribe(listener):註冊監聽器,在 store 變化的時候被調用
  • replaceReducer(nextReducer):更新當前 store 中的 reducer,通常只在開發者模式中使用

2. action:一個 JavaScript 對象,用於描述一個事件(描述發生了什麼)和須要改變的數據,必須有一個 type 字段,用來標識指令,其餘元素是傳送這個指令的 state 值。由組件觸發,並傳送到 reducer

{
  type: "ADD_TODO"
  text: "study Redux"        
}

3. reducer:一個包含 switch 的函數,描述數據如何變化,根據 action type 來進行響應的 state 更新操做(若是沒有更改,則返回當前 state 自己)。整個應用只有一個單一的 reducer 函數,所以須要 combileReducers()函數。

function counter(state = 0, action) {
  switch (action.type) {
    case 'INCREMENT':
      return state + 1;
    case 'DECREMENT':
      return state - 1;
    default:
      return state;
  }
}

 Redux 數據流

                        Redux 數據流圖

這裏給出的是一個簡單的 Redux 數據流圖,基本上能夠描述 Redux 中各個部分是如何運行和協做的,關於每個模塊的具體做用,在下文會結合代碼進行詳細的介紹和分析,相信在看完具體的分析以後,對於上圖你會有必定的理解和新的體會。

容器組件 & 展現組件

  展現組件 容器組件
做用 描述如何展示(標籤、樣式) 描述如何運行(獲取數據、更新狀態)
直接使用 Redux
數據來源 從 this.props 中獲取 使用 connect 從 Redux 狀態樹中獲取
數據修改 調用從 props 中傳入的 action creator 直接分發 action
調用方式 開發者手動建立  由 React Redux 生成

 

 

 

 

 

 

 

 

 

簡單來講,容器型組件描述的是組件如何工做,即數據如何獲取合更新,通常不包含 Virtual DOM 的修改或組合,也不包含組件的樣式

展現型組件描述的是組件是如何渲染的,不依賴 store,通常包含 Virtual DOM 的修改或組合,以及組件的樣式,能夠寫成無狀態函數。

 

在瞭解了上述的一些 Redux 相關的概念,下面將結合實例對 Redux 的使用進行具體的描述和分析。 

TodoList

功能

  • 添加 Todolist
  • 刪除 Todolsit

運行

克隆出上面的 github 的項目後,進入項目,

安裝依賴

npm install

 啓動MongoDB

mongod

 項目打包

webpack -w

 啓動項目

npm start

瀏覽器輸入 localhost:8080,查看效果

效果

目錄結構

入口文件 

index.js

import React from 'react'
import ReactDOM from 'react-dom'
import { createStore, applyMiddleware } from 'redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { Provider } from 'react-redux'
import Todo from './containers/app'
import rootReducer from './reducers/todoReducer'

// 打印日誌方法
const loggerMiddleware = createLogger()

// applyMiddleware() 用來加載 middleWare
const createStoreWithMiddleware = applyMiddleware(thunkMiddleware, loggerMiddleware)(createStore)

// 建立 store 對象
const store = createStoreWithMiddleware(rootReducer)

// 獲取到的 store 是空的?
console.log(store.getState())

// 註冊 subcribe 函數,監聽 state 的每一次變化
const unsubscribe = store.subscribe(() => console.log(store.getState()) );

ReactDOM.render(
    <Provider store={store}>
        <Todo />
    </Provider>,
    document.getElementById("app")
);

 

在入口文件咱們主要作了如下幾件事情:

1. 引入模塊;

2. 使用 thunkMiddleware(用於異步請求數據)和 loggerMiddleware(用於打印日誌) 對 createStore 進行了加強;

3. 而後建立 store 對象,註冊監聽函數(在函數體內能夠添加 state 變化時候的相關操做)

4. 引入 Provider 中間件,做爲根組件的上層容器,接受 store 做爲屬性,將 store 放在 context 中,提供給 connect 組件來鏈接容器組件。

5. 將應用組件掛載到頁面節點上

注:這裏將 store 的相關配置也放在了 入口文件中,爲了使文件結構更加清晰,能夠考慮將 store 的相關配置單獨定義爲 configureStore.js,而後在入口文件中引入。

 

Action

src/actions/todoAction.js

import $ from 'jquery'

// 定義 action type 爲常量
export const INIT_TODO = 'INIT_TODO'
export const ADD_TODO = 'ADD_TODO'
export const DELETE_TODO = 'DELETE_TODO'

// create action
export function initTodo () {
    // 這裏的 action 是一個 Trunk 函數,能夠將 dispatch 和 getState() 傳遞到函數內部
    return (dispatch, getState) => {
        $.ajax({
            url: '/getTodolsit',
            type: 'get',
            dataType: 'json',
            success: data => {
                // console.log(data)
                // 請求成功,分發 action, 這裏的 dispatch 是經過 Redux Trunk Middleware 傳遞過來的
                dispatch({
                    type: 'INIT_TODO',
                    todolist: data.reverse()
                })
            },
            error: () => {
                console.log('獲取 todolist 失敗...')
            }
        })
    }
}

export function addTodo (newTodo) {
    return (dispatch, getState) => {
        $.ajax({
            url: '/addTodo',
            type: 'post',
            dataType: 'json',
            data: newTodo,
            success: data => {
                // console.log(data)
                dispatch({
                    type: 'ADD_TODO',
                    todolist: data.reverse()
                })
            },
            error: () => {
                console.log(err)
            }
        })
    }
}

export function deleteTodo (date) {
    console.log(date)
    return (dispatch, getState) => {
        $.ajax({
            url: '/deleteTodo',
            type: 'post',
            dataType: 'json',
            data: date,
            success: data => {
                // console.log(data)
                dispatch({
                    type: 'DELETE_TODO',
                    todolist: data.reverse()
                })
            },
            error: () => {
                console.log(err)
            }
        })
    }
}

 

能夠看到,這裏的 action 和咱們上面講到的 action 不太同樣,由於這裏用 ajax 進行了數據的異步請求,在前面的入口文件中咱們實用了 trunkMiddleware 中間件(須要在index.js 中引入 redux-trunk),這個中間件就是爲了異步請求用的,對應的異步 action 函數的形式以下:

export const asyncAction () => {
    return (dispatch, getState) => {
        // 在這裏能夠調用異步函數請求數據,並在合適的時機經過 dispatch 參數派發出新的 action 對象
    }
}

 

* redux-trunk 的工做是檢查 action 對象是否是函數,若是不是函數就放行,完成普通的 action 生命週期,若是是函數,則執行函數,並把 Store 的 dispatch 函數和 getState 函數做爲參數傳遞到函數中去,併產生一個同步的 action 對象來對 redux 產生影響。

注:1. 若是涉及到的 action 類型名比較多,能夠將它們單獨定義到一個文件中,而後在這裏引入,以便於後面的管理。

  2. 這裏實用 jQuery 的 ajax 進行數據的請求,也能夠嘗試引入 fetch 進行 Ajax

 

Reducer

reducers/todoReducer.js

import { combineReducers, createStore } from 'redux'
import { INIT_TODO, ADD_TODO, DELETE_TODO } from '../actions/todoAction'

// 在 reducer 第一次執行的時候,沒有任何的 previousState, 所以須要定義一個 initialState,
// 下面使用 es6 的寫法爲 state 賦初始值
function todoReducer (state = [], action) {
  console.log(action);
  switch (action.type) {
    case INIT_TODO:
      return action.todolist
      break
    case ADD_TODO:
      return action.todolist
      break
    case DELETE_TODO:
      return action.todolist
      break
    default:
      return state
  }
}

// 將多個 reducer 合併成一個
const rootReducer = combineReducers({ todoReducer })

export default rootReducer

 

在 reducer 中,首先引入 在 action 中定義的 type 參數,而後定義一個函數(純函數),接收 state 和 action 做爲參數,經過 switch 判斷當前 action type,並返回不一樣的對象(更新 store)。

須要注意的是,當組件剛開始渲染的時候,store 中並無 state,因此針對這種狀況,須要爲 state 賦一個初始值,能夠在函數體內經過 if 語句來判斷,可是 es6 提供了更爲簡潔的寫法,在參數中直接賦值,當傳入的 state 爲空的時候,直接使用初始值,固然這裏默認爲空數組。

combineReducers({...}):首先須要明確的是,整個應用只能有一個 reducer 函數。若是定義的 action type 有不少,那麼針對不一樣的 type,須要寫不少的分支語句或者定義多個 reducer 文件,所以 Redux 提供 combineReducers 函數,來將多個 reducer 合併成一個。

容器組件 

containers/app.js

import React from 'react'
import PropTypes from 'prop-types'
import ReactDOM from 'react-dom'
import { connect } from 'react-redux'
import { initTodo, addTodo, deleteTodo } from '../actions/todoAction'
import TodoList from '../components/todolist'
import TodoForm from '../components/todoform'

class Todo extends React.Component {
    componentDidMount () {
        this.props.dispatch(initTodo())
    }

    handleAddTodo (newTodo) {
        console.log('add new todo......');
        console.log(newTodo);
        this.props.dispatch(addTodo(newTodo))
    }

    handleDeleteTodo (date) {
        const delete_date = { date }
        this.props.dispatch(deleteTodo(delete_date))
    }

    render() {
        // 這裏的 todolist 是在 connect 中以 { todolist: state.todolist } 的形式做爲屬性傳遞給 App 組件的
        const { todolist } = this.props
        console.log(todolist);
          return (
              <div className="container">
                <h2 className="header">Todo List</h2>
                <TodoForm onAddTodo={this.handleAddTodo.bind(this)} />
                <TodoList todolist={todolist} onDeleteTodo={this.handleDeleteTodo.bind(this)} />
              </div>
        )
    }
}

// 驗證組件中的參數類型
Todo.propTypes = {
    todolist: PropTypes.arrayOf(
        PropTypes.shape({
            content: PropTypes.string.isRequired,
            date: PropTypes.string.isRequired
        }).isRequired
    ).isRequired
}

const getTodolist = state => {
    console.log(state);
    return {
        todolist : state.todoReducer
    }
}

export default connect(getTodolist)(Todo)

在容器組件中,咱們定義了頁面的結構(標題、表單、列表),並定義了相關的方法和數據,經過 props 的方式傳遞給對應的子組件。在子組件經過觸發 props 中的回調函數時,在容器組件中接受到就會分發響應的 action,交由 reducer 進行處理(在 reducer 進行狀態的更新,而後同步到組件中,引發視圖的變化)。

添加了propTypes 驗證,這樣在組件中的屬性、方法以及其餘定義的元素的類型不符的時候,瀏覽器會拋出警告,須要注意的是, 和 ReactDOM 同樣, propTypes 已經從 React分離出來了,所以使用的時候須要單獨引入模塊 prop-types(仍然使用 PropTypes from 'React'會有警告,但不影響使用)。

這裏最爲重要的是從 react-redux 中引入了 connect,經過 connect(selector)(App) 來鏈接 store 和 容器組件。其中,selector 是一個函數,接受 store 中的 state 做爲參數,而後返回一個對象,將裏面的參數以屬性的形式傳遞給鏈接的組件,同時還隱式地傳遞 一個 dispatch 方法,做爲組件的屬性。以下所示:

須要注意的是,connect 函數產生的組件是一個高階組件,其完整的形式以下:

const mapStateToProps = (state) => {
    return {
        data: state
    }
}
const mapDispatchToProps = (dispatch) => {
    return {
        getCityWeather: (args) => {
            dispatch(asyncAction(args))
        }
    }
}
export default connect(mapStateToProps, mapDispatchToProps)(App)

 

能夠看出,connect 函數接受兩個參數:

1. mapStateToProps:  將 Store 上的狀態轉化爲展現組件上的 props

2. mapDispatchToProps:將 Store 上的dispatch 動做轉化爲展現組件上的 props

所以, App 組件能夠得到 store 中的 state並傳遞給子組件,並能夠經過 dispatch() 方法來分發 action。以下所示:

展現組件

由容器組件中的 DOM 結構能夠看出主要有 todoform todolist 兩個組件,同時在 todolist 組件中,又再次劃分出了 todo 組件,展現組件比較簡單,不擁有本身的狀態,主要是從父級獲取 props,並在 DOM 中進行展現,同時在組件中觸發事件,經過 this.props.eventHandler()的方式來通知父級,最後觸發 action 實現狀態的修改。

components/todoform.js

import React from 'React'
import PropTypes from 'prop-types'

class TodoForm extends React.Component {

  // 表單輸入時隱藏提示語
  handleKeydown () {
    this.refs.tooltip.style.display = 'none'
  }
  
  // 提交表單操做
    handleSubmit (e) {
        e.preventDefault();
        // 表單輸入爲空驗證
        if(this.refs.content.value == '') {
            this.refs.content.focus()
      this.refs.tooltip.style.display = 'block'
            return ;
        }

    // 獲取時間並格式化
        let month = new Date().getMonth() + 1;
        let date = new Date().getDate();
        let hours = new Date().getHours();
        let minutes = new Date().getMinutes();
        let seconds = new Date().getSeconds();
        if (hours < 10) { hours += '0'; }
        if (minutes < 10) { minutes += '0'; }
        if (seconds < 10) { seconds += '0'; }

        // 生成參數
        const newTodo = {
            content: this.refs.content.value,
            date: month + "/" + date + " " + hours + ":" + minutes + ":" + seconds
        };
        this.props.onAddTodo(newTodo)
        this.refs.todoForm.reset();
    }

  render () {
    return (
      <form className="todoForm" ref="todoForm" onSubmit={ this.handleSubmit.bind(this) }>
        <input ref="content" onKeyDown={this.handleKeydown.bind(this)} type="text" placeholder="Type content here..." className="todoContent" />
        <span className="tooltip" ref="tooltip">Content is required !</span>
      </form>
    )
  }
}

export default TodoForm

 components/todolist.js

import React from 'react';
import PropTypes from 'prop-types'
import Todo from './todo';

class TodoList extends React.Component {

      render() {
          const todolist = this.props.todolist;
      console.log(todolist);
          const todoItems = todolist.map((item, index) => {
              return (
          <Todo
                        key={index}
                        content={item.content}
                        date={item.date}
                        onDeleteTodo={this.props.onDeleteTodo}
          />
            )
      });

        return (
            <div>
                { todoItems }
            </div>
        )
      }
}

// propTypes 用於規範 props 的類型與必需的狀態,在開發環境下會對組件的 props 進行檢查,
// 若是不能與之匹配,將會在控制檯報 warning。在生產環境下不會進行檢查。(解決 JS 弱語言類型的問題)

// arrayOf 表示數組類型, shape 表示對象類型
TodoList.propTypes = {
    todolist: PropTypes.arrayOf(
        PropTypes.shape({
            content: PropTypes.string.isRequired,
            date: PropTypes.string.isRequired,
        }).isRequired
    ).isRequired
}

export default TodoList;

 components/todo.js

import React from 'react'
import PropTypes from 'prop-types'

class TodoItem extends React.Component {

    handleDelete () {
        const date = this.props.date;
        this.props.onDeleteTodo(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>
        )
    }
}

TodoItem.propTypes = {
    content: PropTypes.string.isRequired,
    date: PropTypes.string.isRequired,
    // handleDelete: PropTypes.func.isRequired
}

export default TodoItem;

 

數據庫操做

database/db.js

var mongoose = require('mongoose')

// 定義數據模式,指定保存到 todo 集合
const TodoSchema = new mongoose.Schema({
    content: {
        type: String, 
        required: true
    },
    date: {
        type: String, 
        required: true
    }
}, { collection: 'todo' })

// 定義數據集合的模型
const Todo = mongoose.model('TodoBox', TodoSchema)

module.exports = Todo

這裏就比較簡單了,只有兩個字段,都是 String 類型,並指定保存到 todo 這個集合中,最後經過一行代碼編譯成對應的模型並導出,這樣在 node 中就能夠經過模型來操做數據庫了。

注:由於項目比較簡單,只涉及一個數據集合,因此直接將 schema 和 model 寫在一個文件中,若是涉及多個數據集合,建議將 schema 和 model 放在不一樣的文件中

接口封裝

routes/index.js

var express = require('express');
var Todo = require('../src/database/db')
var router = express.Router();

router.get('/', (req, res, next) => {
    res.render('index', {
        title: 'React TodoList'
    });
});

// 獲取 todolist
router.get('/getTodolsit', (req, res, next) => {
    Todo.find({}, (err,todolist) => {
        if (err) {
            console.log(err);
        }else {
            console.log(todolist);
            res.json(todolist);
        }
    })
});

// 添加 todo
router.post('/addTodo', (req, res, next) => {
    const 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('/deleteTodo', (req, res, next) => {
    const delete_date = req.body.date

    Todo.remove({date: delete_date}, (err, result) => {
        if (err) {
            console.log(err)
        }else {
            // 從新獲取 todolist
            Todo.find({}, (err, todolist) => {
                if (err) {
                    console.log(err);
                }else {
                    res.json(todolist);
                }
            })
        }
    });
});

module.exports = router;

沒有任何魔法,只是簡單的數據庫增刪改查的操做,封裝成接口,來供 createAction 中經過 Ajax 來請求調用。

而後是 webpack 的配置和 CSS 的編寫,都比較簡單,和未使用 Redux 重構的代碼沒有任何修改,因此也就不貼代碼了。

測試

由於使用了 loggerMiddleware 中間件, 能夠跟蹤 actoin 的變化並在瀏覽器控制檯中打印出 state 信息,所以能夠十分直觀地看到數據的變化。

下面就根據這裏的打印信息,結合 React 的生命週期,來簡單捋一遍 Redux 的工做流程。

進入頁面

能夠看到,打印出來的 state 是一個對象,而且最開始是空的數組對象,這是由於在頁面還沒有渲染完畢的時候,即在 componentWillMount 階段,頁面並無任何的 state,直到渲染結束,即 componentDidMount 階段,容器組件主動的觸發了 INIT_TODO 的 action,reducer 接受到這個 action 後開始請求數據,更新 state,而後同步到頁面上來,這也是爲何打印出來的 state 在 todoReducer 這個對象中,由於 state 就是在 reducer 中進行處理和返回的。

添加 todo

能夠看到,這裏觸發了 ADD_TODO 的 action,在執行 reducer 的操做後,state 的中的數據由 6 條變成了 7 條。

刪除 todo

一樣,執行刪除操做時觸發了 DELETE_TODO 的 action,在執行 reducer 的操做後,state 的中的數據由 7 條變成了 6 條。

總結

1. Redux 是一個"可預測的狀態容器",由 Store、Action、Reducer 三部分組成。

2. Store 負責存儲狀態,經過 createStore(reducer, initialState) 生成。

3. Action 中聲明瞭數據的結構,不提供邏輯,在 createAciton 結合中間件能夠發出異步請求。

4. Reducer 是一個純函數,每一個應用只能有惟一的一個 reducer, 多個 reducer 使用 combineReducers() 方法進行合併。

5. react-redux 提供了 <Provider />組件和 connect () 方法實現 Redux 和 React 的綁定。

6. <Provider />接受一個 store 做爲 props,是整個應用的頂層組件;connect () 提供了在整個 React 應用中熱議組件獲取 store 中數據的功能。

7. 容器型組件和 Redux 進行交互並獲取狀態,分發 action;展現型組件從傳入的 props 中獲取數據,經過容器組件下發的的回調函數來觸發事件,向上通知父級,從而觸發 action,實現狀態的更新。

相關文章
相關標籤/搜索