redux —— 入門實例 TodoListhtml
前端技術真是突飛猛進,搞完 React 不搭配個數據流都很差意思了。
滿懷期待的心去翻了翻 flux,簡直被官方那意識流的文檔折服了,真是又臭又長仍是我智商問題??
轉戰 redux ,越看越有意思,跟着文檔作了個 TodoList 的入門小例子。前端
廢話很少說,先貼上文章用到例子的源碼 https://github.com/TongchengQiu/TodoList-as-redux-demo
redux 的 Github 倉庫 https://github.com/rackt/redux
還有個中文的 gitbook 翻譯文檔 http://camsong.github.io/redux-in-chinese/index.htmlnode
隨着spa(不是SPA,是單頁應用)的發展,以 react 來講,組件化和狀態機的思想真是解放了煩惱的 dom 操做,一切都爲狀態。state 來操縱 views 的變化。
然而,由於頁面的組件化,致使每一個組件都必須維護自身的一套狀態,對於小型應用還好。
可是對於比較大的應用來講,過多的狀態顯得錯綜複雜,到最後難以維護,很難清晰地組織全部的狀態,在多人開發中也是如此,致使常常會出現一些不明因此的變化,越到後面調試上也是越麻煩,不少時候 state 的變化已經不受控制。對於組件間通行、服務端渲染、路由跳轉、更新調試,咱們很須要一套機制來清晰的組織整個應用的狀態,redux 應然而生,這種數據流的思想真是了不得。react
在 react 中,咱們儘可能會把狀態放在頂層的組件,在頂層組件使用 redux 或者 router。
這就把組件分爲了兩種:容器組件和展現組件。
容器組件:和 redux 和 router 交互,維護一套狀態和觸發 action。
展現組件:展現組件是在容器組件的內部,他們不維護狀態,全部數據經過 props 傳給他們,全部操做也是經過回調完成。
這樣,咱們整套應用的架構就顯得清晰了。webpack
redux 分爲三大部分,store , action ,reducer 。git
整個應用的 state 被儲存在一棵 object tree 中,而且這個 object tree 只存在於惟一一個 store 中。
或者這麼說 store 的指責有這些:github
維護整個應用的 stateweb
提供 getState() 方法獲取 state;npm
提供 dispatch(action) 方法更新 state;json
經過 subscribe(listener) 註冊監聽器。
這麼解釋一下,整個應用的 state 都儲存在 store 中,容器組件能夠從 store 中獲取所須要的狀態。
容器組件同時也能夠發送給 store 一個 action,告訴他改變某個狀態的值,因此說容器組件只要發送一個指令,就能夠叫 store 去 setState,而後 store 的 state 改變,回過來容器組件獲取到的 state 改變,致使 views 的更新。
action 能夠理解爲一種指令,store 數據的惟一由來就是 action,action 是一個對象,它須要至少一個元素,type,type 是這個指令的惟一標識,其它元素是傳送這個指令的 state 值
{ type: ACTION_TYPE, text: 「content」, }
這個指令由組件觸發,而後傳到 reducer。
action 只是說明了要去作什麼,和作這件事情須要的參數值。
具體去改變 store 中的 state 是由 reducer 來作的。
reducer 實際上是一個包含 switch 的函數,前面不是說組件觸發的 action 會傳遞到 reducer,reducer 接收這個參數 action,他經過 switch(action.type) 而後作不一樣操做,前面說了,這個 type 是指令的標識,reducer 根據這個標識來做出不一樣的操做。
這個操做是什麼呢?
reducer 還接收另外一個參數 state,這個是舊的 state。從 action 裏面還能夠獲取到作這個操做須要的 參數值。
這個操做其實就是對原有的 state 和 從 action 中的到的值,來進行操做(結合,刪除,...)而後返回一個 新的 state 到 store。
把前面的語言組織一下,整個操做的數據流實際上是這樣的:
store 把整個應用的 state,getState(),dispatch(),subscribe() 傳給頂層容器組件;
容器組件和三個部分交互:
內部的展現組件:容器把狀態分發給各個組件,把 dispatch(操做數據的函數)以回調的形式分發給各個組件;
action:容器獲取 action;
reducer:容器能夠調用 dispatch(action),這個上面說了,會以回調的形式給下面的子組件,這樣就能夠根據不一樣的用戶操做,調用不一樣的 dispatch(action),執行了這個函數以後,就把 action 傳給 reducer,而後看 reducer;
reducer 獲得容器組件傳來的 action 以後,根據 action.type 這個參數執行不一樣操做,他還會接收到 store 裏面的原 state,而後把原 state 和 action 對象裏面的其它參數進行操做,而後 return 一個新的對象。
reducer return 一個新的對象到 store,store 根據這個新對象,更新應用狀態。
----一個循環 ♻️
Redux 和 React 之間沒有關係,他們並補互相依賴,可是 Redux 和 React 搭配起來簡直完美。
咱們能夠經過 react-redux 這個庫把他們綁定起來
npm install --save react-redux
react-redux 提供兩個東西 Provider 和 connect。
這個 Provider 其實就是一箇中間件,他是在原有 App Container 上面再包一層,他的做用就是接收 store 裏面的 store 做爲 props,將store放在context裏,給下面的connect用的。
這個組件纔是真正鏈接 Redux 和 React,他包在咱們的容器組件的外一層,他接收上面 Provider 提供的 store 裏面的 state 和 dispatch,傳給一個構造函數,返回一個對象,以屬性形式牀給咱們的容器組件。
這個項目使用 webpack 來構建,想要了解 webpack 的配置能夠看個人其它兩篇文章:
如何使用webpack—webpack-howto;
webpack-best-practice-最佳實踐-部署生產.
. ├── app #開發目錄 | | | ├──actions #action的文件 | | | ├──components #內部組件 | | | ├──containers #容器組件 | | | ├──reducers #reducer文件 | | | ├──stores #store配置文件 | | | └──index.js #入口文件 | ├── dist #發佈目錄 ├── node_modules #包文件夾 ├── .gitignore ├── .jshintrc ├── server.js #本地靜態服務器 ├── webpack.config.js #webpack配置文件 └── package.json
這裏,咱們只關注咱們的 app 開發目錄。
import React from 'react'; import { render } from 'react-dom'; import { Provider } from 'react-redux'; import App from './containers/App'; import configureStore from './stores/configureStore'; const store = configureStore(); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') );
這裏咱們從 react-redux 中獲取了一個 Provider 組件,咱們把它渲染到應用的最外層。
他須要一個屬性 store ,他把這個 store 放在context裏,給App(connect)用。
app/stores.configureStore.js
import { createStore } from 'redux'; import rootReducer from '../reducers'; export default function configureStore(initialState) { const store = createStore(rootReducer, initialState); if (module.hot) { module.hot.accept('../reducers', () => { const nextReducer = require('../reducers'); store.replaceReducer(nextReducer); }); } return store; }
他從 redux 拿到 createStore 這個函數,再獲取到 rootReducer ;
createStore 函數接收兩個參數,(reducer, [initialState]),reducer 毋庸置疑,他須要從 store 獲取 state,以及鏈接到 reducer 交互。
initialState 是能夠自定義的一個初始化 state,可選參數。module.hot
這個能夠不用管,這是 webpack 熱加載的處理,你也能夠不要他。
containers/App.jsx
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import { addTodo, completeTodo, setVisibilityFilter, VisibilityFilters } from '../actions'; import AddTodo from '../components/AddTodo'; import TodoList from '../components/TodoList'; import Footer from '../components/Footer'; class App extends Component { render() { const { dispatch, visibleTodos, visibilityFilter } = this.props; return ( <div> <AddTodo onAddClick={text => dispatch(addTodo(text)) } /> <TodoList todos={visibleTodos} onTodoClick={index => dispatch(completeTodo(index))} /> <Footer filter={visibilityFilter} onFilterChange={nextFilter => dispatch(setVisibilityFilter(nextFilter))} /> </div> ); } } App.propTypes = { visibleTodos: PropTypes.arrayOf(PropTypes.shape({ text: PropTypes.string.isRequired, completed: PropTypes.bool.isRequired })), visibilityFilter: PropTypes.oneOf([ 'SHOW_ALL', 'SHOW_COMPLETED', 'SHOW_ACTIVE' ]).isRequired }; function selectTodos(todos, filter) { switch (filter) { case VisibilityFilters.SHOW_ALL: return todos; case VisibilityFilters.SHOW_COMPLETED: return todos.filter(todo => todo.completed); case VisibilityFilters.SHOW_ACTIVE: return todos.filter(todo => !todo.completed); } } // 這裏的 state 是 Connect 的組件的 function select(state) { return { visibleTodos: selectTodos(state.todos, state.visibilityFilter), visibilityFilter: state.visibilityFilter }; } export default connect(select)(App);
他從 react-redux 獲取 connect 鏈接組件,經過 connect(select)(App)
鏈接 store 和 App 容器組件。
select 是一個函數,他能接收到一個 state 參數,這個就是 store 裏面的 state,而後經過這個函數的處理,返回一個對象,把對象裏面的參數以屬性傳送給 App,以及附帶一個 dispatch。
因此在 App 裏面能夠:
const { dispatch, visibleTodos, visibilityFilter } = this.props;
因此 App 經過 connect 的到 state 和 dispatch,把 state 傳遞給子組件。
dispatch 這個函數能夠接收一個 action 參數,而後就會執行 reducer 裏面的操做。
好比:
text => dispatch(addTodo(text))
addTodo(text)
,這個函數是在 action 裏面的到的,能夠看 action 的代碼,他其實返回一個 action 對象,因此其實就是dispatch(action)
。
app/actions/index.js
export const ADD_TODO = 'ADD_TODO'; export const COMPLETE_TODO = 'COMPLETE_TODO'; export const SET_VISIBILITY_FILTER = 'SET_VISIBILITY_FILTER'; export const VisibilityFilters = { SHOW_ALL: 'SHOW_ALL', SHOW_COMPLETED: 'SHOW_COMPLETED', SHOW_ACTIVE: 'SHOW_ACTIVE', }; export function addTodo(text) { return { type: ADD_TODO, text }; } export function completeTodo(index) { return { type: COMPLETE_TODO, index }; } export function setVisibilityFilter(filter) { return { type: SET_VISIBILITY_FILTER, filter }; }
在聲明每個返回 action 函數的時候,咱們須要在頭部聲明這個 action 的 type,以便好組織管理。
每一個函數都會返回一個 action 對象,因此在 容器組件裏面 調用
text => dispatch(addTodo(text))
就是調用dispatch(action)
。
app/reducers/visibilityFilter.js
import { SET_VISIBILITY_FILTER, VisibilityFilters } from '../actions'; const { SHOW_ALL } = VisibilityFilters; function visibilityFilter(state = SHOW_ALL, action) { switch (action.type) { case SET_VISIBILITY_FILTER: return action.filter; default: return state; } } export default visibilityFilter;
這裏咱們從 actions 得到各個 type 的參數,以便和 action 作好映射對應。
整個函數其實就是執行 switch,根據不一樣的 action.type,返回不一樣的對象狀態。
可是若是咱們須要 type 不少,好比除了 visibilityFilter,還有 todos,難道要寫一個長長的switch,固然不。
redux 提供一個 combineReducers 輔助函數,把一個由多個不一樣 reducer 函數做爲 value 的 object,合併成一個最終的 reducer 函數,而後就能夠對這個 reducer 調用 createStore。
咱們把不一樣的 reducer 放在不一樣文件下。app/reducers/todo.js
import { ADD_TODO, COMPLETE_TODO } from '../actions'; function todos(state = [], action) { switch (action.type) { case ADD_TODO: return [ ...state, { text: action.text, completed: false } ]; case COMPLETE_TODO: return [ ...state.slice(0, action.index), Object.assign({}, state[action.index], { completed: true }), ...state.slice(action.index + 1) ]; default: return state } } export default todos;
而後經過一個 index.js 把他們合併。app/reducers/index.js
import { combineReducers } from 'redux'; import todos from './todos'; import visibilityFilter from './visibilityFilter'; const rootReducer = combineReducers({ todos, visibilityFilter }); export default rootReducer;
app/components/AddTodo/index.jsx
import React, { Component, PropTypes } from 'react'; import { findDOMNode } from 'react-dom'; export default class AddTodo extends Component { render() { return ( <div> <input type='text' ref='input' /> <button onClick={ e => this.handleClick(e) }> Add </button> </div> ); } handleClick(e) { const inputNode = findDOMNode(this.refs.input); const text = inputNode.value.trim(); this.props.onAddClick(text); inputNode.value = ''; } } AddTodo.propTypes = { onAddClick: PropTypes.func.isRequired };
app/components/Todo/index.jsx
import React, { Component, PropTypes } from 'react'; export default class Todo extends Component { render() { const { onClick, completed, text } = this.props; return ( <li onClick={onClick} style={{ textDecoration: completed ? 'line-through' : 'none', cursor: completed ? 'default' : 'pointer' }} > {text} </li> ); } } Todo.propTypes = { onClick: PropTypes.func.isRequired, text: PropTypes.string.isRequired, completed: PropTypes.bool.isRequired };
app/components/TodoList/index.jsx
import React, { Component, PropTypes } from 'react'; import Todo from '../Todo'; export default class TodoList extends Component { render() { return ( <ul> { this.props.todos.map((todo, index) => <Todo {...todo} onClick={() => this.props.onTodoClick(index)} key={index} /> ) } </ul> ); } } TodoList.propTypes = { onTodoClick: PropTypes.func.isRequired, todos: PropTypes.arrayOf(PropTypes.shape({ text: PropTypes.string.isRequired, completed: PropTypes.bool.isRequired }).isRequired).isRequired };
app/components/Footer/index.jsx
import React, { Component, PropTypes } from 'react'; export default class Footer extends Component { renderFilter(filter, name) { if(filter == this.props.filter) { return name; } return ( <a href="#" onClick={e => { e.preventDefault(); this.props.onFilterChange(filter); }}> {name} </a> ); } render() { return ( <p> SHOW {' '} {this.renderFilter('SHOW_ALL', 'All')} {', '} {this.renderFilter('SHOW_COMPLETED', 'Completed')} {', '} {this.renderFilter('SHOW_ACTIVE', 'Active')} . </p> ); } } Footer.propTypes = { onFilterChange: PropTypes.func.isRequired, filter: PropTypes.oneOf([ 'SHOW_ALL', 'SHOW_COMPLETED', 'SHOW_ACTIVE' ]).isRequired };
能夠看出,全部的展現組件須要的 state 和 數據,都從屬性中獲取的,全部的操做,都是經過容器組件給的回調函數來操做的。
他們儘量地不擁有本身的狀態,作無狀態組件。
關於 redux 的用法,這只是基礎入門的部分,還有的多的搞基操做,好比異步數據流、Middleware、和 router 配合。
敬請期待~~~~
???????????????????
個人博客原文redux 大法好 —— 入門實例 TodoList;