總括: 本文采用react+redux+react-router+less+es6+webpack,以實現一個簡易備忘錄(todolist)爲例儘量全面的講述使用react全家桶實現一個完整應用的過程。javascript
代碼地址:React全家桶實現一個簡易備忘錄html
原文博客地址:React全家桶實現一個簡易備忘錄前端
博主博客地址:Damonare的我的博客node
人生不失意,焉能暴己知。react
技術架構:本備忘錄使用react+react-router+redux+less+ES6+webpack實現;webpack
頁面UI參照:TodoList官網實現;git
在線演示地址:Damonare的備忘錄;es6
支持回車添加新事項;github
支持刪除事項(點擊X符號);
支持狀態轉換具體包括:
新建事項->正在進行(點擊checkbox選項)
正在進行->已完成(點擊文字內容自己)
正在進行->新建事項(點擊checkbox選項)
已完成->正在進行(點擊文字自己)
支持判斷輸入空字符,過長字符(20個漢字之內);
支持搜索;
支持本地化存儲;
支持狀態的展開隱藏(點擊標題)
兼容手機端(iPhone6及以上)
支持路由切換
毫無疑問,當談到React
的時候不能避免的會提到組件化思想。React剛開始想解決的問題只是UI這一層面的問題,也就是MVC中view層面的問題,不成想現在越滾越大,從最先的UI引擎變成了一整套先後端通吃的 Web App 解決方案。對於React
組件的理解一樣要站在view層面的角度出發,一個完整的頁面是由大大小小的組件堆疊而成,就好像搭積木,每一塊積木都是一個組件,組件套組件組成了用戶所能看到的完整的頁面。
使用React
,不必定非要使用JSX
語法,可使用原生的JS進行開發。可是React
做者強烈建議咱們使用JSX
,由於JSX
在定義相似HTML這種樹形結構時,十分的簡單明瞭。這裏簡單的講下JSX
的由來。
好比,下面一個div元素,咱們用HTML語法描述爲:
<div class="test"> <span>Test</span> </div>
若是換作使用javascript描述這個元素呢?最好的方式能夠簡單的轉化爲json
對象,以下:
{ type:"div", props:{ className:"test", children:{ type:"span", props:{ children:"Test" } } } }
這樣咱們就能夠在javascript中建立一個Virtual DOM
(虛擬DOM)了。固然,這樣是無法複用的,咱們再把它封裝一下:
const Div=>({text}){ return { type:"div", props:{ className:"test", children:{ type:"span", props:{ children: text, }, }, }, } }
接下來再實現這個div就能夠直接調用Div('Test')來建立。但上述結構看起來實在讓人不爽,寫起來也很容易寫混,一旦結構複雜了,很容易讓人找不着北,因而JSX
語法應運而生。咱們用寫HTML的方式寫這段代碼,再通過翻譯器轉換成javascript後交給瀏覽器執行。上述代碼用JSX
重寫:
const Div =()=>( <div className="test"> <span>Test</span> </div> );
多麼簡單明瞭!!!具體的JSX語法
很少說了,學習更多戳這:JSX in Depth
其實上面已經提到了Virtual DOM
,它的存在也是React
長久不衰的緣由之一,虛擬DOM的概念並非FB獨創卻在FB的手上大火了起來(後臺是多麼重要)。
咱們知道真實的頁面對應了一個DOM樹,在傳統頁面的開發模式中,每次須要更新頁面時,都須要對DOM進行更新,DOM操做十分昂貴,爲減小對於真實DOM的操做,誕生了Virtual DOM
的概念,也就是用javascript把真實的DOM樹描述了一遍,使用的也就是咱們剛剛說過的JSX
語法。對好比下:
每次數據更新以後,從新計算Virtual DOM
,並和上一次的Virtual DOM
對比,對發生的變化進行批量更新。React也提供了shouldComponentUpdate
生命週期回調,來減小數據變化後沒必要要的Virtual DOM
對比過程,提高了性能。
Virtual DOM
雖然渲染方式比傳統的DOM操做要好一些,但並不明顯,由於對比DOM節點也是須要計算的,最大的好處在於能夠很方便的和其它平臺集成,好比react-native
就是基於Virtual DOM
渲染出原生控件。具體渲染出的是Web DOM
仍是Android
控件或是iOS
控件就由平臺決定了。因此咱們說react
的出現是一場革命,一次對於native app
的宣戰,就像react-native
那句口號——Learn Once,Write Anywhere.
過去編程方式主要是以命令式編程爲主,什麼意思呢?簡單說電腦的思惟方式和咱們人類的思考方式是不同的。咱們人類的大腦擅長的是分析問題,提出一個解決問題的方案,電腦則是生硬的執行指令,命令式編程就像是給電腦下達命令,讓電腦去執行同樣,如今主要的編程語言(好比:Java,C,C++等)都是由命令式編程構建起來的。
而函數式編程就不同了,這是模仿咱們人類的思惟方式發明出來的。例如:操做某個數組的每個元素而後返回一個新數組,若是是計算機的思考方式,會這樣想:建立一個新數組=>遍歷舊數組=>給新數組賦值。若是是人類的思考方式,會這樣想:建立一個數組方法,做用在舊數組上,返回新數組。這樣此方法能夠被重複利用。而這就是函數式編程了。
在React中,數據的流動是單向的,即從父節點傳遞到子節點。也所以組件是簡單的,他們只須要從父組件獲取props渲染便可。若是頂層的props改變了,React會遞歸的向下遍歷整個組件樹,從新渲染全部使用這個屬性的組件。那麼父組件如何獲取子組件數據呢?很簡單,經過回調就能夠了,父組件定義某個方法供給子組件調用,子組件調用方法傳遞給父組件數據,Over。
這東西我以爲沒啥難度,官方例子都很不錯,跟着官方例子來一遍基本就明白究竟是個啥玩意了,官方例子:react-router-tutorial。
完事之後能夠再看一下阮一峯老師的教程,主要是對一些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(首相)的職責:
維持應用的 state;
提供 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 個步驟:
Redux store 調用傳入的 reducer 函數。
根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
Redux store 保存了根 reducer 返回的完整 state 樹。
工做流程圖以下:
這裏須要再強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。
儘管如此,Redux 仍是和 React 和 Deku 這類框架搭配起來用最好,由於這類框架容許你以 state 函數的形式來描述界面,Redux 經過 action 的形式來發起 state 變化。
Redux 默認並不包含 React 綁定庫,須要單獨安裝。
npm install --save react-redux
固然,咱們這個實例裏是不須要的,全部須要的依賴已經在package.json裏配置好了。
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') )
上面代碼中,Provider
在根組件外面包了一層,這樣一來,App
的全部子組件就默認均可以拿到state
了。
講解以前能夠先看一下github上的代碼,你能夠clone下來學習,也能夠在線給我提issue,歡迎戳這:React全家桶實現簡易備忘錄
. ├── app #開發目錄 | | | ├──actions #action的文件 | | | ├──components #展現組件 | | | ├──containers #容器組件,主頁 | | | ├──reducers #reducer文件 | | | |——routes #路由文件,容器組件 | | | |——static #靜態文件 | | | ├──stores #store配置文件 | | | |——main.less #路由樣式 | | | └──main.js #入口文件 | ├── build #發佈目錄 ├── node_modules #包文件夾 ├── .gitignore ├── .jshintrc ├── webpack.production.config.js #生產環境配置 ├── webpack.config.js #webpack配置文件 ├── package.json #環境配置 └── README.md #使用說明
接下來,咱們只關注app目錄就行了。
import React from 'react'; import ReactDOM from 'react-dom'; import {Route, IndexRoute, browserHistory, Router} from 'react-router'; import {createStore} from 'redux'; 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={browserHistory}> <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.body.appendChild(document.createElement('div')))
這裏咱們從 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, Change_Done_To_Doing, Search } from '../actions'; let todos; (function() { if (localStorage.todos) { todos = JSON.parse(localStorage.todos) } else { todos = [] } })(); function todolist(state = todos, action) { switch (action.type) { /* * 添加新的事項 * 並進行本地化存儲 * 使用ES6展開運算符連接新事項和舊事項 * JSON.stringify進行對象深拷貝 */ case Add_Todo: localStorage.setItem('todos', JSON.stringify([ ...state, { todo: action.text, istodo: true, doing: false, done: false } ])); 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 Change_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;
具體的展現組件這裏就不羅列代碼了,感興趣的能夠戳這:備忘錄展現組件地址
嚴格來講,這個備忘錄並非使用的react全家桶,畢竟還有一部分less代碼,不過這一個應用也算是比較全面的使用了react+react-router+redux,做爲react全家桶技術學習的練手的小項目再適合不過了。若是您對這個小東西感興趣,歡迎戳這:React全家桶實現簡易備忘錄給個star。