總括: 本文采用react+redux+react-router+less+es6+webpack,以實現一個簡易備忘錄(todolist)爲例儘量全面的講述使用react全家桶實現一個完整應用的過程。javascript
人生不失意,焉能暴己知。html
技術架構:本備忘錄使用react+react-router+redux+less+ES6+webpack實現;前端
頁面UI參照:TodoList官網實現;java
在線演示地址:Damonare的備忘錄;react
當談到React
的時候不能避免的會提到組件化思想。React剛開始想解決的問題是UI層面的問題,view層面的問題。後來越滾越大,從最先的UI引擎變成了一整套先後端通吃的 Web App 解決方案。一個完整的頁面是由大大小小的組件堆疊而成,組件套組件組成了用戶所能看到的完整的頁面。webpack
使用React
,不必定非要使用JSX
語法,可使用原生的JS進行開發。可是React
做者強烈建議咱們使用JSX,以下:
git
簡單明瞭!!!具體的JSX語法
很少說了,學習更多戳這:JSX es6
虛擬DOM的概念並非FB獨創卻在FB的手上大火了起來。github
頁面對應了一個DOM樹,在傳統頁面的開發模式中,每次須要更新頁面時,都須要對DOM進行更新,DOM操做十分昂貴,爲減小對於真實DOM的操做,誕生了Virtual DOM
的概念,也就是用javascript把真實的DOM樹描述了一遍,使用的也就是咱們剛剛說過的JSX
語法。web
對好比下:
每次數據更新以後,從新計算Virtual DOM
,並和上一次的Virtual DOM
對比,對發生的變化進行批量更新。React也提供了shouldComponentUpdate
生命週期回調,來減小數據變化後沒必要要的Virtual DOM
對比過程,提高了性能。
Virtual DOM
雖然渲染方式比傳統的DOM操做要好一些,但並不明顯,由於對比DOM節點也是須要計算的,最大的好處在於能夠很方便的和其它平臺集成,好比react-native
就是基於Virtual DOM
渲染出原生控件。具體渲染出的是Web DOM
仍是Android
控件或是iOS
控件就由平臺決定了。這一點上讓咱們多平臺的編碼至關方便。
過去編程方式主要是以命令式編程爲主。電腦生硬的執行指令,給電腦下達命令,電腦去執行,如今主要的編程語言(好比:Java,C,C++等)都是由命令式編程構建起來的。
而函數式編程就不同了,這是模仿咱們人類的思惟方式發明出來的。例如:操做某個數組的每個元素而後返回一個新數組,若是是計算機的思考方式,會這樣想:建立一個新數組=>遍歷舊數組=>給新數組賦值。若是是人類的思考方式,會這樣想:建立一個數組方法,做用在舊數組上,返回新數組。這樣此方法能夠被重複利用。而這就是函數式編程了。
state state是組件內部的狀態,當組件內部使用內置的setState方法,該組件會從新進行渲染。 注意一下,setState方法是一個異步的方法,在一個生命週期中的全部setState方法會合並操做。 props props是React中用來讓組件之間相互聯繫的一種機制,props的傳遞過程對React是很是直觀的,React的單向數據流主要的流動管道就是props,props自己是不可變的,當咱們試圖改變props的原始值,React會報類型錯誤的警告。組件的props必定是來源於默認指定的屬性或者是從父組件傳入的。 React爲props提供了默認配置,經過defaultProps靜態變量的方式來定義,當組件被調用的時候,默認值保證渲染後始終有值。 在React中有一個內置的props--children,表明的是子組件的集合,根據子組件的數量,this.props.children的數據類型並且不一致,當沒有子組件的時候爲undefined,當有一個的時候爲object,當有多個的時候爲array。 propTypes propTypes是用來規範props的類型和必須的狀態,若是組件定義了propTypes,那麼在開發環境下會對props進行檢查,在生產環境下是不會進行檢查的。
完事之後能夠再看一下阮一峯老師的教程,主要是對一些API的講解:React Router 使用教程。
隨着 JavaScript 單頁應用開發日趨複雜,JavaScript 須要管理比任什麼時候候都要多的 state (狀態)。 這些 state 可能包括服務器響應、緩存數據、本地生成還沒有持久化到服務器的數據,也包括 UI 狀態,如激活的路由,被選中的標籤,是否顯示加載動效或者分頁器等等。若是一個 model 的變化會引發另外一個 model 變化,那麼當 view 變化時,就可能引發對應 model 以及另外一個 model 的變化,依次地,可能會引發另外一個 view 的變化。亂!
這時候Redux
就強勢登場了,如今你能夠把React
的model看做是一個個的子民,每個子民都有本身的一個狀態,紛紛擾擾,各自維護着本身狀態,太亂了,咱們須要一個King來領導你們,咱們就能夠把Redux
看做是這個King。網羅全部的組件組成一個國家,掌控着一切子民的狀態!防止有人叛亂生事!
這個時候就把組件分紅了兩種:容器組件(King或是路由)和展現組件(子民)。
redux
或是router
,起到了維護狀態,出發action的做用,其實就是King高高在上下達指令。props
傳給他,全部操做經過回調完成
展現組件 | 容器組件 | |
---|---|---|
做用 | 描述如何展示(骨架、樣式) | 描述如何運行(數據獲取、狀態更新) |
直接使用 Redux | 否 | 是 |
數據來源 | props | 監聽 Redux state |
數據修改 | 從 props 調用回調函數 | 向 Redux 派發 actions |
調用方式 | 手動 | 一般由 React Redux 生成 |
Redux三大部分:store
,action
,reducer
。至關於King的直系下屬。
那麼也能夠看出Redux
只是一個狀態管理方案,徹底能夠單獨拿出來使用,這個King不只僅能夠是React的,去Angular,Ember那裏也是能夠作King的。在React中維繫King和組件關係的庫叫作 react-redux
。
, 它主要有提供兩個東西:Provider
和connect
,具體使用文後說明。
提供幾個Redux的學習地址:官方教程-中文版,Redux 入門教程(一):基本用法
Store 就是保存數據的地方,它其實是一個Object tree
。整個應用只能有一個 Store。這個Store能夠看作是King的首相,掌控一切子民(組件)的活動(state)。
Redux 提供createStore
這個函數,用來生成 Store。
import { createStore } from 'redux'; const store = createStore(func);
createStore接受一個函數做爲參數,返回一個Store對象(首相誕生記)
咱們來看一下Store(首相)的職責:
getState()
方法獲取 state;dispatch(action)
方法更新 state;subscribe(listener)
註冊監聽器;subscribe(listener)
返回的函數註銷監聽器。State 的變化,會致使 View 的變化。可是,用戶接觸不到 State,只能接觸到 View。因此,State 的變化必須是 View 致使的。Action 就是 View 發出的通知,表示 State 應該要發生變化了。即store的數據變化來自於用戶操做。action就是一個通知,它能夠看做是首相下面的郵遞員,通知子民(組件)改變狀態。它是 store 數據的惟一來源。通常來講會經過 store.dispatch()
將 action 傳到 store。
Action 是一個對象。其中的type
屬性是必須的,表示 Action 的名稱。
const action = { type: 'ADD_TODO', payload: 'Learn Redux' };
Action建立函數
Action 建立函數 就是生成 action 的方法。「action」 和 「action 建立函數」 這兩個概念很容易混在一塊兒,使用時最好注意區分。
在 Redux 中的 action 建立函數只是簡單的返回一個 action:
function addTodo(text) { return { type: ADD_TODO, text } }
這樣作將使 action 建立函數更容易被移植和測試。
Action 只是描述了有事情發生了這一事實,並無指明應用如何更新 state。而這正是 reducer 要作的事情。也就是郵遞員(action)只負責通知,具體你(組件)如何去作,他不負責,這事情只能是大家村長(reducer)告訴你如何去作。
專業解釋: Store 收到 Action 之後,必須給出一個新的 State,這樣 View 纔會發生變化。這種 State 的計算過程就叫作 Reducer。
Reducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State。
const reducer = function (state, action) { // ... return new_state; };
下圖展現了Redux帶來的變化。
Redux 應用中數據的生命週期遵循下面 4 個步驟:
store.dispatch(action)
。工做流程圖以下:
component -> action -> store -> reducer -> state 的單向數據流,歸納的說就是:React組件裏面獲取了數據以後(好比ajax請求),而後建立一個action通知store我有這個想改變state的意圖,而後reducers(一個action可能對應多個reducer,能夠理解爲action爲訂閱的主題,可能有多個訂閱者)來處理這個意圖而且返回新的state,接下來store會收集到全部的reducer的state,最後更新state。
這裏須要再強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
儘管如此,Redux 仍是和 React 和 Deku 這類框架搭配起來用最好,由於這類框架容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。
Redux 默認並不包含 React 綁定庫,須要單獨安裝。
npm install --save react-redux
React-Redux
提供connect
方法,用於從 UI 組件生成容器組件。connect
的意思,就是將這兩種組件連起來。
import { connect } from 'react-redux';
const TodoList = connect()(Memos);
上面代碼中Memos
是個UI組件,TodoList
就是由 React-Redux 經過connect
方法自動生成的容器組件。
而只是純粹的這樣把Memos包裹起來毫無心義,完整的connect方法這樣使用:
import { connect } from 'react-redux' const TodoList = connect( mapStateToProps )(Memos)
上面代碼中,connect
方法接受兩個參數:mapStateToProps
和mapDispatchToProps
。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state
映射到 UI 組件的參數(props
),後者負責輸出邏輯,即將用戶對 UI 組件的操做映射成 Action。
這個Provider 實際上是一箇中間件,它是爲了解決讓容器組件拿到King的指令(state
對象)而存在的。
import { Provider } from 'react-redux' import { createStore } from 'redux' import todoApp from './reducers' import App from './components/App' let store = createStore(todoApp); render( <Provider store={store}> <App /> </Provider>, document.getElementById('root') )
講解以前能夠先看一下github上的代碼,你能夠clone下來學習,也能夠在線給我提issue,歡迎戳這:React全家桶實現簡易備忘錄
4.1目錄結構
接下來,咱們只關注app目錄就行了。
app/main.jsx
import React from 'react'; import ReactDOM from 'react-dom'; import { Route, IndexRoute, hashHistory, Router } from 'react-router'; import { Provider } from 'react-redux'; import App from './container/App'; import AllMemosRoute from './routes/AllMemosRoute'; import TodoRoute from './routes/TodoRoute'; import DoingRoute from './routes/DoingRoute'; import DoneRoute from './routes/DoneRoute'; import configureStore from './stores'; import './main.less'; const store = configureStore(); ReactDOM.render( <Provider store={store}> <Router history={hashHistory}> <Route path="/" component={App}> <IndexRoute component={AllMemosRoute} /> <Route path="/todo" component={TodoRoute} /> <Route path="/doing" component={DoingRoute} /> <Route path="/done" component={DoneRoute} /> </Route> </Router> </Provider>, document.getElementById('root') );
這裏咱們從react-redux
中獲取到 Provider 組件,咱們把它渲染到應用的最外層。
他須要一個屬性 store ,他把這個 store 放在context裏,給Router(connect)用。
app/store/index.jsx
import { createStore } from 'redux'; import reducer from '../reducers'; export default function configureStore(initialState) { const store = createStore(reducer, initialState); if (module.hot) { // Enable Webpack hot module replacement for reducers module.hot.accept('../reducers', () => { const nextReducer = require('../reducers'); store.replaceReducer(nextReducer); }); } return store; }
app/action/index.jsx
'use strict'; /* * @author Damonare 2016-12-10 * @version 1.0.0 * action 類型 */ export const Add_Todo = 'Add_Todo'; export const Change_Todo_To_Doing = 'Change_Todo_To_Doing'; export const Change_Doing_To_Done = 'Change_Doing_To_Done'; export const Change_Done_To_Doing = 'Change_Done_To_Doing'; export const Change_Doing_To_Todo = 'Change_Doing_To_Todo'; export const Search='Search'; export const Delete_Todo='Delete_Todo'; /* * action 建立函數 * @method addTodo添加新事項 * @param {String} text 添加事項的內容 */ export function addTodo(text) { return { type: Add_Todo, text } } /* * @method search 查找事項 * @param {String} text 查找事項的內容 */ export function search(text) { return { type: Search, text } } /* * @method changeTodoToDoing 狀態由todo轉爲doing * @param {Number} index 須要改變狀態的事項的下標 */ export function changeTodoToDoing(index) { return { type: Change_Todo_To_Doing, index } } /* * @method changeDoneToDoing 狀態由done轉爲doing * @param {Number} index 須要改變狀態的事項的下標 */ export function changeDoneToDoing(index) { return { type: Change_Done_To_Doing, index } } /* * @method changeDoingToTodo 狀態由doing轉爲todo * @param {Number} index 須要改變狀態的事項的下標 */ export function changeDoingToTodo(index) { return { type: Change_Doing_To_Todo, index } } /* * @method changeDoingToDone 狀態由doing轉爲done * @param {Number} index 須要改變狀態的事項的下標 */ export function changeDoingToDone(index) { return { type: Change_Doing_To_Done, index } } /* * @method deleteTodo 刪除事項 * @param {Number} index 須要刪除的事項的下標 */ export function deleteTodo(index) { return { type: Delete_Todo, index } }
在聲明每個返回 action 函數的時候,咱們須要在頭部聲明這個 action 的 type,以便好組織管理。
每一個函數都會返回一個 action 對象,因此在 組件裏面調用
text =>
dispatch(addTodo(text))
就是調用dispatch(action)
。
app/reducers/index.jsx
import { combineReducers } from 'redux'; import todolist from './todos'; // import visibilityFilter from './visibilityFilter'; const reducer = combineReducers({ todolist, }); export default reducer;
app/reducers/todos.jsx
import { ADD_TODO, DELETE_TODO, CHANGE_TODO_TO_DOING, CHANGE_DOING_TO_DONE, CHANGE_DOING_TO_TODO, CAHNGE_DONE_TO_DOING, SEARCH, } from '../actions'; let todos; (() => { if (localStorage.todos) { todos = JSON.parse(localStorage.todos); } else { todos = []; } })(); const todolist = (state = todos, action) => { switch (action.type) { /* * 添加新的事項 * 並進行本地化存儲 * 使用ES6展開運算符連接新事項和舊事項 * JSON.stringify進行對象深拷貝 */ case ADD_TODO: return [ ...state, { todo: action.text, istodo: true, doing: false, done: false } ]; /* * 將todo轉爲doing狀態,注意action.index的類型轉換 */ case CHANGE_TODO_TO_DOING: localStorage.setItem('todos', JSON.stringify([ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: true, done: false }, ...state.slice(parseInt(action.index) + 1) ])); return [ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: true, done: false }, ...state.slice(parseInt(action.index) + 1) ]; /* * 將doing轉爲done狀態 */ case CHANGE_DOING_TO_DONE: localStorage.setItem('todos', JSON.stringify([ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: false, done: true }, ...state.slice(parseInt(action.index) + 1) ])); return [ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: false, done: true }, ...state.slice(parseInt(action.index) + 1) ]; /* * 將done轉爲doing狀態 */ case CAHNGE_DONE_TO_DOING: localStorage.setItem('todos', JSON.stringify([ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: true, done: false }, ...state.slice(parseInt(action.index) + 1) ])); return [ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: false, doing: true, done: false }, ...state.slice(parseInt(action.index) + 1) ]; /* * 將doing轉爲todo狀態 */ case CHANGE_DOING_TO_TODO: localStorage.setItem('todos', JSON.stringify([ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: true, doing: false, done: false }, ...state.slice(parseInt(action.index) + 1) ])); return [ ...state.slice(0, action.index), { todo:state[action.index].todo, istodo: true, doing: false, done: false }, ...state.slice(parseInt(action.index) + 1) ]; /* * 刪除某個事項 */ case DELETE_TODO: localStorage.setItem('todos', JSON.stringify([ ...state.slice(0, action.index), ...state.slice(parseInt(action.index) + 1) ])); return [ ...state.slice(0, action.index), ...state.slice(parseInt(action.index) + 1) ]; /* * 搜索 */ case SEARCH: let text = action.text; let reg = eval("/"+text+"/gi"); return state.filter(item=> item.todo.match(reg)); default: return state; } } export default todolist;