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

Damonare發表於2017-01-15

前言

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

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

技術說明

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

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

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

功能說明

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

正文

1. React淺談

1.1 元件化

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

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語法。對比如下:

React 全家桶實現一個簡易備忘錄
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 樹

工作流程圖如下:

React 全家桶實現一個簡易備忘錄
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。

相關文章