详解 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,实现状态的更新。

相关文章
相关标签/搜索