Redux之旅-2

時間:2016.4.22-11:24
作者:三月懶驢
入門配置文章:鏈接
Redux之旅-1:鏈接

1. 前言

上一篇文章,很簡單的用代碼來說明了State,Action,Reducer,Store各自的意義和相互的聯系以及在實際開發的應用。但是在現實開發環境用,我們的需求不僅僅上一篇文章那么簡單的。為了應付這些復雜的開發環境,我們還需要用到以下這些東西:

  • Action Creator / bindActionCreators:動態去生成Action。
  • combineReducers:分割你的reducers,使每一個reducer更有意義,之后再合并回來。
  • applyMiddleware / createStoreWithMiddleware:往store里面添加中間件。使得它的功能更加強大。

很明顯,我們將要學習的這三個功能,對應回上一篇文章的Action,reducer,store。你可以認為這是它們各自的一些擴展。

2. 新的需求

在上一篇文章我們的需求很簡單。就是一個按鈕,點擊就讓數字遞增,那么今天為了學習新的東西,我們就增加一些新的需求。我們需求加兩個按鈕,一個按鈕就是點擊數字是+2,一個按鈕是切換數字顏色。那么為了更方便我們的開發,我們的開發目錄也應該修改一下。

主要寫代碼的還是在app這個文件夾里面,里面的目錄分布

  • component -->放置組件的
    • index.js
  • redux -->放置redux
    • action -->action相關的
      • types.js
      • creator.js
    • reducer -->reducer相關的
      • rootReducer.js
      • increaseReducer.js --> 處理增加按鈕用的
      • themeReducer.js --> 處理切換主題按鈕用的
    • store -->store相關的
      • store.js
    • index.js -->把組件和store connect在一起生成一個新組件
  • main.js -->程序入口

3. Action Creator

3.1 為什么需要Creator

在Redux之旅-1里面,我們創建了一個很簡單的例子:點擊按鈕遞增。那么在更多的時候,我們的開發需求是很復雜的。那么增加一個點擊就+2的按鈕。這時候我們要怎么做?
還記得我們上次定義的Action是這樣的:

let IncreaseAction = {type:'increase'}

其中字符串increase的意思就是增加,那么我們現在的需求里面,一個點擊+2的按鈕,也符合increase這個意思。這時候的Action就不能像之前那樣,直接寫死。因為寫死了就沒辦法讓reducer知道,這個Action是+1還是+2了!而需要引入一個新的概念:Action Creator

let IncreaseAction = (num = 1)=>{
 return{
    type:'increaseAction',
    num  //看注1
 }
}

注1:這里用到了ES6的新語法,增強字面量。具體看文章最后的注釋

上面就是一個Action Creator,聽起來很高大上的英文名詞,其實就是一個幫我們返回Action的函數。那還記得Action的本質其實是什么嗎?一個我們規定最了低限度要帶著type屬性的對象。那么其實就很簡單了。Creator每次幫我們生成type都是increaseAction,但num不同的Action出來。這樣,reducer就能分辨出我們的這個Action要+1還是+2

3.2 代碼實現

文件位置: app/redux/action/types.js

//為了更加直觀,我們把每個Action的type屬性的值額外用一個文件去描述
export const INCREASE = 'increase'  //增加數字的
export const THEME = 'theme'  //切換主題的

文件位置: app/redux/action/creator.js

import * as types from './types'

export let increaseAction = (num = 1)=>{
  return {
    type:types.INCREASE,
    num
  }
}

export let themeAction = ()=>{
  return {
    type:types.THEME,
  }
}

OK!上面的代碼中,我還順便把切換主題的Action也寫了,雖然它不需要傳入參數去處理,但是為了以后的拓展性,用Action Creator總比用寫死了的Action更佳,畢竟產品經理要改需求,誰也擋不住。

4. combineReducers

4.1 分割和合并

和Flux不同。Redux的store只有一個!只有一個store就意味著只有一個state。那么我們設計state的時候就很會糾結了。像現在這樣子:

let state = {
    count:0,  //增加數字
    theme:'#ffffff'  //主題顏色
}

這并不是一個理想的state。一個理想的state應該要設計得很純粹。不應該把無關得兩個東西交錯在一起。因為,我們需要分割開我們的reducer。每個不同的reducer下面僅僅處理自己擁有的state.

let CountState = {
    count:0,  //增加數字
}
let ThemeState = {
    theme:'#ffffff'  //主題顏色
}

這樣的話,我們的state就可以設計得很小,并且很純粹。而作為匯總,我們會用combineReducers把這些reducers合并回來。實際上state還是一大塊,但是在reducer的角度來看,每個reducer都指示一小塊。

4.2 代碼實現

文件位置: app/redux/reducer/increaseReducer.js

//處理數字增加的reducer
import * as types from '../action/types'

let reducer = (state={count:0},action)=>{
    let count = state.count
    switch(action.type){
        case types.INCREASE:
            //注意這里使用的action.num,明白是從哪里來的嗎?
            return {count:count+action.num}
            break
        default:
            return state
    }
}
export default reducer

文件位置: app/redux/reducer/themeReducer.js

//處理主題的reducer
import * as types from '../action/types'

const writeColor = '#ffffff'
const grayColor = '#cccccc'
let reducer = (state={color:writeColor},action)=>{
    let count = state.count
    switch(action.type){
        case types.THEME:
            if(state.color == writeColor){
              return {color:grayColor}
            }
            else {
              return {color:writeColor}
            }
            break
        default:
            return state
    }
}
export default reducer

文件位置: app/redux/reducer/rootReducer.js

//合并兩個reducer
import { combineReducers } from 'redux'
import Increase from './increaseReducer'
import Theme from './themeReducer'

const rootReducer = combineReducers({
    Increase,
    Theme
})

export default rootReducer

5. Middleware

5.1 什么叫做中間件

在這里我并不打算深入去描述中間件這種概念,如果你是做node開發的話,又剛好用上了KOA的話,你會對這個東西很熟識。那么這里我簡單的介紹一下什么是中間件。

你可能認為它是一個使用包含自定義功能的(可以讓你包裝 store 的 dispatch 方法來達到你想要的功能),用來擴展 Redux 是一種方式。

在這里,有兩個中間件是推薦使用的。

//用來打log的,開發時候用很方便
npm install --save-dev redux-logger  
//這個會在異步Action的時候用到,這篇文章沒有到,不細說。
npm install --save redux-thunk

注2:thunk,推薦閱讀阮一峰老師的Thunk 函數的含義和用法

5.2 代碼實現

文件位置: app/redux/store/store.js

'use strick'
import {createStore,applyMiddleware} from 'redux'
import createLogger from 'redux-logger'
import rootReducer from '../reducer/rootReducer'
/*
    以前創建store的方式:
    let store = createStore(reducers)
*/
let createStoreWithMiddleware = applyMiddleware(
  createLogger(),
)(createStore)
let store = createStoreWithMiddleware(rootReducer)

export default store

6. 連接組件和redux

這里面和以前的差不多,但又有一些細節上的東西,我們需要注意的

文件位置: app/redux/index.js


'use strict'

import React from 'react'
//注意這里引入:bindActionCreators
import { bindActionCreators } from 'redux'
import { Provider,connect } from 'react-redux'

//這里引入了組件
import Index from '../component/index'

//引入了action creator 和 store。為啥沒有引入reducer?
//因為reducer在上面創建store的時候已經用到了
import * as actions from './action/creator'
import store from './store/store'

let {Component} = React

/*
    mapStateToProps里面需要注意的是,由于我們的reducer是合并起來的,因此我們的state也是幾個state拼起來。至于是哪幾個state拼起來?
    可以看回去rootReducer.js里面combineReducers的時候,里面的對象名字就是這里state的名字。
    當然這里的return可以寫成:return {state}也沒所謂。但是為了大家能認清這個state里面有什么東西,這里寫的稍微復雜一點
*/
let mapStateToProps = (state) =>{
    //return {state}
    return {
        reduxState:{
            Increase:state.Increase,
            Theme:state.Theme,
        }
    }
}
/*
    mapDispatchToProps里面用到了bindActionCreators。關于bindActionCreators的作用看下面注釋3
*/
let mapDispatchToProps = (dispatch) =>{
    return{
        reduxActions:bindActionCreators(actions,dispatch)
    }
}
let Content = connect(mapStateToProps,mapDispatchToProps)(Index)



class App extends Component{
    render(){
        return <Provider store={store}><Content /></Provider>
    }
}

export default App


注3: bindActionCreators(actionCreators, dispatch):是把 action creators 轉成擁有同名 keys 的對象,而使用 dispatch 把每個 action creator 包圍起來,這樣可以直接調用它們。

7. 組件開發

關于redux的開發在上面已經完成了,現在進入的是組件的開發,這里面更多的是滿足例子而寫的。沒有太多的現實開發價值。但是我們可以在這里面很好的觀察,我們寫好的程序是怎樣工作的。

文件位置: app/component/index.js

'use strict'

import React from 'react'
let {Component}  = React
class Index extends Component{
    constructor(props){
        super(props)
    }
    //點擊增加按鈕事件
    _addHandle(num){
    /*
    這里面可以想一下increaseAction的名字是怎么來的,同樣下面主題切換按鈕事件的themeAction又是怎么來的,代碼之后為你揭秘。
    */
        let {increaseAction} = this.props.reduxActions
        increaseAction(num)
    }
    //主題切換按鈕事件
    _ThemeHandle(){
        let { themeAction } = this.props.reduxActions
        themeAction()
    }
    render(){
        //這里面輸出props看看里面有什么東西
        //console.log(this.props)
        let { reduxState } = this.props
        return (
            <div style={styles.circle}>
                <div style={{color:reduxState.Theme.color}}>{reduxState.Increase.count}</div>
                <div style={styles.btnTheme} onClick={this._ThemeHandle.bind(this)}>切換主題</div>
                <div style={styles.btnAddOne} onClick={()=>{this._addHandle(1)}}>+1</div>
                <div style={styles.btnAddTwo} onClick={()=>{this._addHandle(2)}}>+2</div>
            </div>
        )
    }
}
//樣式定義,不用細看
const styles = {
    circle:{
        width:'400px',
        height:'400px',
        position:'absolute',
        left:'50%',
        top:'50%',
        margin:'-200px 0 0 -200px',
        borderRadius:'50px',
        fontSize:'60px',
        color:'#545454',
        backgroundColor:'#ececec',
        lineHeight:'100px',
        textAlign:'center',
    },
    btnAddOne:{
        width:'100px',
        height:'50px',
        lineHeight:'50px',
        backgroundColor:'#fcfcfc',
        fontSize:'20px',
        position:'absolute',
        left:'40px',
        bottom:'10px',
        cursor:'pointer',
    },
    btnAddTwo:{
        width:'100px',
        height:'50px',
        lineHeight:'50px',
        backgroundColor:'#fcfcfc',
        fontSize:'20px',
        position:'absolute',
        right:'40px',
        bottom:'10px',
        cursor:'pointer',
    },
    btnTheme:{
        width:'80%',
        height:'50px',
        lineHeight:'50px',
        backgroundColor:'#fcfcfc',
        fontSize:'20px',
        position:'absolute',
        right:'10%',
        bottom:'100px',
        cursor:'pointer',
    }
}
export default Index

需要注意:在redux里面action/creator/reducer/store各種定義太多了。有時候你很難找到這個東西的名稱是哪里定義過來的感覺。輸出組件的props的話,我們得到這么一個東西

{
    reduxAction:{
        increaseAction:fn(),
        themeAction:fn(),
    },
    reduxState:{
      Increase:count,
      Theme:color,
    }
}
  • reduxAction:來自redux/index.js里面,我們mapDispatchToProps的時候return 的一個對象時候自己定義的一個對象名字

    • increaseAction:fn()/themeAction:fn():因為bindActionCreators的轉化造成的和creator的同名函數。因此這兩個函數的名稱定義出自redux/action/creator.js
  • reduxState:來自redux/index.js里面,我們mapStateToProps的時候return 的一個對象時候自己定義的一個對象名字

    • Increase/Theme 也是在mapStateToProps里面自己定義的。

這里順便貼出入口文件

文件位置: app/main.js

'use strict'
import 'babel-polyfill'
import React from 'react';
import { render } from 'react-dom'

import App from './redux/index';

render(
    <App />,
    document.getElementById('main')
);

如果沒有做錯,我們的程序能跑起來,如同下圖一般

效果圖-1

而控制臺會在每次按鈕之后,會如同下圖一樣

效果圖-2

8. 總結

和上一次說明state/Action/reducer/store的各種比如不同,這次需要說的分別是Action/reducer/store的一些擴展,為了更好的寫程序,這些擴展是必不可少的,但會給新手一種把程序切割得很零散從而無從下手得感覺。所以這次我更加偏重于說明,每一個擴展存在得意義以及代碼得實現,希望大家能看懂。

9.一些ES6的小知識

在3.1的注1的時候,我們說了一個ES6的新特性:字面量增強
ES6里面我們定義一個對象的時候

let c = 'abc'
let obj = {
   a,   //這個叫做鍵名簡寫
   b(){   
       //這個是函數定義的簡寫
       .... 
   },
   [c]:12   // 這個是自定義鍵值寫法,一個很有趣的語法糖
}

等于號后面我們叫做 字面量
以上代碼等價于以下

let obj = {
   a:a,    
   b:function(){   
      ....
   },
   abc:12    
}
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,936評論 6 535
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,744評論 3 421
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 176,879評論 0 381
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,181評論 1 315
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,935評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,325評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,384評論 3 443
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,534評論 0 289
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,084評論 1 335
  • 茶點故事閱讀 40,892評論 3 356
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,067評論 1 371
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,623評論 5 362
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,322評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,735評論 0 27
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,990評論 1 289
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,800評論 3 395
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,084評論 2 375

推薦閱讀更多精彩內容