組件化的開發思想解放了繁瑣低效的 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 應用架構>
1. 單一數據源。
應用只有惟一的數據源,整個應用的狀態都保存在一個對象中,爲提取出整個應用的狀態進行持久化提供可能,同時 Redux 提供的 combineReducers 方法對數據源過於龐大的問題進行了有效的化解。
2. 狀態是隻讀的。
在 Redux 中,沒法直接經過 setState() 來修改狀態,而是經過定義 reducer ,根據當前觸發的 action 類型對當前的 state 進行迭代。reducer(previousState, action) => newState
3. 狀態修改由純函數完成。
狀態修改經過 reducer 來實現,每個 reducer 都是純函數,當接受必定的 state 和 action,返回的 newState 都是固定不變的。
1. store:由 createStore(reducer,initialState)方法生成,用於維護整個應用的 state。store 包含如下四個方法:
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 的使用進行具體的描述和分析。
克隆出上面的 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,而後在入口文件中引入。
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
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 實現狀態的修改。
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 中進行處理和返回的。
能夠看到,這裏觸發了 ADD_TODO 的 action,在執行 reducer 的操做後,state 的中的數據由 6 條變成了 7 條。
一樣,執行刪除操做時觸發了 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,實現狀態的更新。