基於react-app搭建react-router+redux項目

前言

總括: 本文采用react+redux+react-router+less+es6+webpack,以實現一個簡易備忘錄(todolist)爲例儘量全面的講述使用react全家桶實現一個完整應用的過程。javascript

人生不失意,焉能暴己知。html

技術說明

技術架構:本備忘錄使用react+react-router+redux+less+ES6+webpack實現;前端

頁面UI參照:TodoList官網實現;java

在線演示地址:Damonare的備忘錄;react

功能說明

  • 支持回車添加新事項;
  • 支持刪除事項(點擊X符號);
  • 支持狀態轉換具體包括:
    • 新建事項->正在進行(點擊checkbox選項)
    • 正在進行->已完成(點擊文字內容自己)
    • 正在進行->新建事項(點擊checkbox選項)
    • 已完成->正在進行(點擊文字自己)
  • 支持判斷輸入空字符,過長字符(20個漢字之內);
  • 支持搜索;
  • 支持本地化存儲;
  • 支持狀態的展開隱藏(點擊標題)
  • 兼容手機端(iPhone6及以上)
  • 支持路由切換

1. React淺談

1.1 組件化

當談到React的時候不能避免的會提到組件化思想。React剛開始想解決的問題是UI層面的問題,view層面的問題。後來越滾越大,從最先的UI引擎變成了一整套先後端通吃的 Web App 解決方案。一個完整的頁面是由大大小小的組件堆疊而成,組件套組件組成了用戶所能看到的完整的頁面。webpack

1.2 JSX語法糖

​ 使用React,不必定非要使用JSX語法,可使用原生的JS進行開發。可是React做者強烈建議咱們使用JSX,以下:git

 
 

簡單明瞭!!!具體的JSX語法很少說了,學習更多戳這:JSX es6

1.3 Virtual DOM

虛擬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控件就由平臺決定了。這一點上讓咱們多平臺的編碼至關方便。

1.4 函數式編程

​ 過去編程方式主要是以命令式編程爲主。電腦生硬的執行指令,給電腦下達命令,電腦去執行,如今主要的編程語言(好比:Java,C,C++等)都是由命令式編程構建起來的。

​ 而函數式編程就不同了,這是模仿咱們人類的思惟方式發明出來的。例如:操做某個數組的每個元素而後返回一個新數組,若是是計算機的思考方式,會這樣想:建立一個新數組=>遍歷舊數組=>給新數組賦值。若是是人類的思考方式,會這樣想:建立一個數組方法,做用在舊數組上,返回新數組。這樣此方法能夠被重複利用。而這就是函數式編程了。

1.5 數據流

在React中,數據是單向流動的,是從上向下的方向,即從父組件到子組件的方向。
state和props是其中重要的概念,若是頂層組件初始化props,那麼React會向下遍歷整顆組件樹,從新渲染相關的子組件。其中state表示的是每一個組件中內部的的狀態,這些狀態只在組件內部改變。
把組件當作是一個函數,那麼他接受props做爲參數,內部由state做爲函數的內部參數,返回一個虛擬dom的實現。
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進行檢查,在生產環境下是不會進行檢查的。

 

1.6 React主要就下圖之中的這些內容,掌握了這個圖就算入門了。

 

2. React-router

官方例子:react-router-tutorial。

完事之後能夠再看一下阮一峯老師的教程,主要是對一些API的講解:React Router 使用教程

3. Redux 

3.1 簡介

隨着 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 入門教程(一):基本用法

3.2 Store

Store 就是保存數據的地方,它其實是一個Object tree。整個應用只能有一個 Store。這個Store能夠看作是King的首相,掌控一切子民(組件)的活動(state)。

Redux 提供createStore這個函數,用來生成 Store。

import { createStore } from 'redux'; const store = createStore(func);

createStore接受一個函數做爲參數,返回一個Store對象(首相誕生記)

咱們來看一下Store(首相)的職責:

3.3 action

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 建立函數更容易被移植和測試。

 

3.4 reducer

Action 只是描述了有事情發生了這一事實,並無指明應用如何更新 state。而這正是 reducer 要作的事情。也就是郵遞員(action)只負責通知,具體你(組件)如何去作,他不負責,這事情只能是大家村長(reducer)告訴你如何去作。

專業解釋: Store 收到 Action 之後,必須給出一個新的 State,這樣 View 纔會發生變化。這種 State 的計算過程就叫作 Reducer。

Reducer 是一個函數,它接受 Action 和當前 State 做爲參數,返回一個新的 State。

const reducer = function (state, action) { // ...
  return new_state; };

3.5 數據流

下圖展現了Redux帶來的變化。

 

Redux 應用中數據的生命週期遵循下面 4 個步驟:

  • 調用 store.dispatch(action)
  • Redux store 調用傳入的 reducer 函數。
  • 根 reducer 應該把多個子 reducer 輸出合併成一個單一的 state 樹。
  • Redux store 保存了根 reducer 返回的完整 state 樹

工做流程圖以下:

 component -> action -> store -> reducer -> state 的單向數據流,歸納的說就是:React組件裏面獲取了數據以後(好比ajax請求),而後建立一個action通知store我有這個想改變state的意圖,而後reducers(一個action可能對應多個reducer,能夠理解爲action爲訂閱的主題,可能有多個訂閱者)來處理這個意圖而且返回新的state,接下來store會收集到全部的reducer的state,最後更新state。

3.6 Connect

這裏須要再強調一下: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方法接受兩個參數:mapStateToPropsmapDispatchToProps。它們定義了 UI 組件的業務邏輯。前者負責輸入邏輯,即將state映射到 UI 組件的參數(props),後者負責輸出邏輯,即將用戶對 UI 組件的操做映射成 Action。

 

3.7 Provider

 這個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') )

 

4.實戰備忘錄

講解以前能夠先看一下github上的代碼,你能夠clone下來學習,也能夠在線給我提issue,歡迎戳這:React全家桶實現簡易備忘錄

 4.1目錄結構

 

接下來,咱們只關注app目錄就行了。

4.2入口文件  

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)用。

4.3 Store

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; }

 

4.4 Action 建立函數和常量

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) 。

 

4.5 Reducers 

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;
相關文章
相關標籤/搜索