React 全家桶實現一個簡易備忘錄

前言

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

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

技術說明

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

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

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

功能說明

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

正文

1. React淺談

1.1 組件化

​ 毫無疑問,當談到React的時候不能避免的會提到組件化思想。React剛開始想解決的問題只是UI這一層面的問題,也就是MVC中view層面的問題,不成想現在越滾越大,從最先的UI引擎變成了一整套先後端通吃的 Web App 解決方案。對於React組件的理解一樣要站在view層面的角度出發,一個完整的頁面是由大大小小的組件堆疊而成,就好像搭積木,每一塊積木都是一個組件,組件套組件組成了用戶所能看到的完整的頁面。github

1.2 JSX語法糖

​ 使用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

1.3 Virtual DOM

其實上面已經提到了Virtual DOM,它的存在也是React長久不衰的緣由之一,虛擬DOM的概念並非FB獨創卻在FB的手上大火了起來(後臺是多麼重要)。

咱們知道真實的頁面對應了一個DOM樹,在傳統頁面的開發模式中,每次須要更新頁面時,都須要對DOM進行更新,DOM操做十分昂貴,爲減小對於真實DOM的操做,誕生了Virtual DOM的概念,也就是用javascript把真實的DOM樹描述了一遍,使用的也就是咱們剛剛說過的JSX語法。對好比下:

Virtual DOM原理

每次數據更新以後,從新計算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.

1.4 函數式編程

​ 過去編程方式主要是以命令式編程爲主,什麼意思呢?簡單說電腦的思惟方式和咱們人類的思考方式是不同的。咱們人類的大腦擅長的是分析問題,提出一個解決問題的方案,電腦則是生硬的執行指令,命令式編程就像是給電腦下達命令,讓電腦去執行同樣,如今主要的編程語言(好比:Java,C,C++等)都是由命令式編程構建起來的。

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

1.5 數據流

在React中,數據的流動是單向的,即從父節點傳遞到子節點。也所以組件是簡單的,他們只須要從父組件獲取props渲染便可。若是頂層的props改變了,React會遞歸的向下遍歷整個組件樹,從新渲染全部使用這個屬性的組件。那麼父組件如何獲取子組件數據呢?很簡單,經過回調就能夠了,父組件定義某個方法供給子組件調用,子組件調用方法傳遞給父組件數據,Over。

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

, 它主要有提供兩個東西:Providerconnect,具體使用文後說明。

提供幾個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 樹

工做流程圖以下:

redux工做原理圖

3.6 Connect

這裏須要再強調一下:Redux 和 React 之間沒有關係。Redux 支持 React、Angular、Ember、jQuery 甚至純 JavaScript。

儘管如此,Redux 仍是和 ReactDeku 這類框架搭配起來用最好,由於這類框架容許你以 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方法接受兩個參數: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')
)複製代碼

上面代碼中,Provider在根組件外面包了一層,這樣一來,App的全部子組件就默認均可以拿到state了。

4.實戰備忘錄

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

4.1目錄結構

.
├── 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目錄就行了。

4.2入口文件

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

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

相關文章
相關標籤/搜索