Redux在項目中的文件結構

React + Redux

  今天我們來嘮嘮在React一般項目中,使用Redux進行狀態管理的時候,相對的如何存放reducer、action、api之類文件的結構與使用時機吧。本章默認看官們已經有初步使用過redux。

一般項目

  部落客說的一般項目,指的是只需要一個state倉庫來進行狀態管理的項目,適合一般公司項目製作、個人學習等。這樣子的項目不需要額外的combineReducers來整合你的大量state倉庫,只需要一個單一state倉庫來進行數據管理。

  值得注意的是,這裡的大量state並不是不滿足redux單一倉庫原則,只不過由於涉及的數據較多,且由於需求分割、功能分割,導致每個state倉庫需要儲存在不同的文件,方便尋找及修改,同時減少單個state倉庫體量,能夠有效減少空間佔用,避免每次store.getState(),都會取一次巨大的state倉庫。

  那麼一般體量state倉庫的項目,我們應該如何比較好的整理我們的文件結構呢?

  首先當需要進行倉庫的state修改時,我們會修改哪裡呢?reducer需要修改,action需要修改,action的請求type也需要修改,那麼我們可以將其統一放置在store文件夾下,將其作為一個整體,這樣當需要修改時,能夠很快的定位到該位置。

/src
    /api
        server.js
        api.js
    /page
    /store
        actionCreators.js
        actionTypes.js
        index.js
        reducer.js
    index.js  

  以上是我的部分文件結構,我們來看一下內部我們是如何設計各文件結構的吧,在這個環節,我會將我之前的一個項目的這部分文件展示出來,並盡量打出詳細注釋,希望大家能有所收穫。

  OK,那咱們先從最基本的入口文件src/index.js開始吧。

index.js(入口文件)

// 必要的React導入
import React from 'react';
import ReactDOM from 'react-dom';
// 必要的React導入
import { createStore } from 'redux'
// 創建好的store
import store from './store'
// 創建好的router
import MyRouter from './pages/router'

ReactDOM.render(
    // 綁定到整個項目
    <MyRouter store={store} />,
    document.getElementById('root')
);

  這是整個項目的入口文件,非常的簡潔,當你需要回看項目的時候,查看有關redux的部分可以直接進入store/index.js進行查看

store/actionTypes.js

// 控制左側導航欄伸出收入
export const CONTROL_LEFT_PAGE = 'controlLeftPage';
// 控制右側主題欄伸出收入
export const CONTROL_RIGHT_PAGE = 'controlRightPage';

  為什麼我們會需要一個這樣子的JS文件呢?我們難道不可以直接在action里使用{type: 'controlRightPage'}來作為type標識嗎?

  完全沒問題!但是這麼寫,總是有他的原因的,讓我們來看一個簡單的例子。

簡單的例子

  某天,我有一個新需求,有個動作需要對state裡面的數據進行修改,需要在reducer里默認一個string作為type,為"setInputVal"。很正常的需求,那我們開始咯?我們在reducer裡面需要寫一個類似以下結構的東西。

// 這是一個reducer
export default (state = defaultState, action) => {
    let newState;
    switch (action.type) {
        case 'setInputVal':
            newState = JSON.parse(JSON.stringify(state));
            newState.leftPageFlag = action.flag;
            return newState;
    }
    // 如果沒有對應的action.type,返回的是未修改的state
    return state;
}

  相對應的,我們在action里會有一個類似以下結構的東西。

// 這是一個action
export const setInput = (val) => ({
    type: 'setInputValue',
    val
})

  表面看上去並沒有什麼差錯,但是當你手誤輸入錯了你的type,上方我就模擬了我們coding時的一個錯誤,會出現什麼呢?對的,這個地方並不會報錯,你的程式將會正常的執行,但是你會發現項目里關於這個功能的操作是無效的,於是你一遍一遍的查看你的每一行邏輯程式碼,最後發現,嗯,我這行寫錯了,改過來就好了。

  我們再來看看假如使用自定義的參數來保存type會發生什麼?

// 這是一個自定義的type參數
export const SET_INPUT_VAL = 'setInputVal';
// 這是一個reducer
export default (state = defaultState, action) => {
    let newState;
    switch (action.type) {
        case SET_INPUT_VAL:
            newState = JSON.parse(JSON.stringify(state));
            newState.leftPageFlag = action.flag;
            return newState;
    }
    // 如果沒有對應的action.type,返回的是未修改的state
    return state;
}
// 這是一個action
export const setInput = (val) => ({
    type: SET_INPUT_VAL,
    val
})

  這是大家可能發現了,你會發現你非常難有出錯的機會因為type創建的時候是使用常量定義的,整個程式只使用一次setInputVal,後續你將他各種重命名也是與整個程式完全沒有關係的。

  而假如你將SET_INPUT_VAL寫錯成了SET_INPUT_VALUE,你的程式會告訴你,SET_INPUT_VALUE is not defined。這句話是有多麼的美妙,畢竟coding都是會有人為上的差錯的,但是當你出錯的時候有東西為你指明了修改的方向你會覺得非常舒服,人生又有了方向 (部落客也經常為一個變數名或者string修改BUG能把鍵盤扣掉。)

store/reducer.js

// 導入你創建的type
import { CONTROL_LEFT_PAGE, CONTROL_RIGHT_PAGE } from './actionTypes'
/**
 * 這是一個state倉庫
 **/
const defaultState = {
    // 左側導航flag
    leftPageFlag: false,
    // 右側導航flag
    rightPageFlag: false,
};

// 這是你的reducer,獲得默認倉庫或傳入一個倉庫,根據action.type來進行相應修改
export default (state = defaultState, action) => {
    let newState;
    switch (action.type) {
        // 這裡的CONTROL_LEFT_PAGE其實就是一個自己定義的string類型的字元串
        case CONTROL_LEFT_PAGE:
            newState = JSON.parse(JSON.stringify(state));
            newState.leftPageFlag = action.flag;
            return newState;
        case CONTROL_RIGHT_PAGE:
            newState = JSON.parse(JSON.stringify(state));
            newState.rightPageFlag = action.flag;
            return newState;
    }
    return state;
}

  將此state倉庫放在這個地方的原因是因為,修改reducer的時候,會有一個關於state的參照,可以清楚的看到自己希望修改的是上面的state的哪一部分。同時switch——case的寫法也很會很直觀。

store/actionCreators.js

// 導入你創建的type
import { CONTROL_LEFT_PAGE, CONTROL_RIGHT_PAGE } from './actionTypes'
// api文件,這塊想解釋的是redux-thunk的作用
import { getStarArticlesApi } from '../api/api'
// 導入你的store
import store from '../store'

//工廠模式

/**
 * 控制左側導航欄伸出收入
 **/
export const controlLeftPage = (flag) => ({
    type: CONTROL_LEFT_PAGE,
    // 傳入的參數,進入reducer後根據這個邏輯進行state的修改
    flag
})

/**
 * 控制右側主題欄伸出收入
 **/
export const controlRightPage = (flag) => ({
    type: CONTROL_RIGHT_PAGE,
    // 傳入的參數,進入reducer後根據這個邏輯進行state的修改
    flag
})

/**
 * 獲取明星文章
 **/
//  注意,這個getStarArticles並不是真正的action,只是作為一個包容非同步操作後進行action的一個函數。
export const getStarArticles = (req) => {
    return (dispatch) => {
        // getStarArticles執行後,進行http請求
        getStarArticlesApi(req).then(res => {
            // 請求完畢,將結果通過action傳給reducer
            const action = getStarArticlesBack(res);
            dispatch(action);
        }).catch(err => {
            // 報錯
            console.log(err);
        })
    }
}

/**
 * 獲取明星文章的回調
 **/
export const getStarArticlesBack = (res) => ({
    type: GET_STAR_ARTICLES,
    // 傳入的參數,進入reducer後根據這個邏輯進行state的修改
    res
})

  actionCreators是創建你的action的地方,當你需要增加action的時候你都可以在actionTypes.js文件中定義type後,在這個文件定義你的action

  如果是需要在動作中執行http請求的話,redux本身是不能夠做到這一點的,所以我們引入了redux-thunk這個npm庫,它允許我們在dispatch action前對action做一些處理,比如一些非同步操作(http請求),所以這部分我留了getStarArticles這個action來給大家舉一個例子。

store/index.js

// 必要的redux方法
import { createStore, applyMiddleware, compose } from 'redux';
// 我的一個reducer
import reducer from './reducer'
// redux-thunk是請求的中間件,當我們在講action部分會提到它
import thunk from 'redux-thunk' 

//增強函數 一步方法,執行兩個函數
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
    window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

//中間件 
const enhancer = composeEnhancers(applyMiddleware(thunk));

// 整合store
const store = createStore(
    reducer, /* preloadedState, */
    enhancer
);

// 導出
export default store;

  index.js文件的話其實基本邏輯是固定的,這個文件的作用是整合reducer(可能還有中間件thunk),並將其作為一個store對象輸出的,這裡的store如果大家不夠了解的話可以移步我的另一個博文:Redux的createStore實現

api/server.js

import axios from 'axios'
import qs from 'qs'

let http = {
    post: '',
    get: ''
}

http.post = function (api, data) {
    let params = qs.stringify(data);
    return new Promise((resolve, reject) => {
        axios.post(api, params).then(res => {
            resolve(res.data)
        }).catch(err => {
            reject(err.data)
        })
    })
}

http.get = function (api, data) {
    let params = qs.stringify(data);
    return new Promise((resolve, reject) => {
        axios.get(api, params).then(res => {
            resolve(res.data)
        }).catch(err => {
            reject(err.data)
        })
    })
}
export default http

  這個位置沒有做過多的注釋,因為這個其實是我個人的一個對自身而言比較熟悉的axios封裝,所以這部分大家只要知道,導出的http是一個對象,裡面有兩個對象方法分別是getpost,返回的都是一個Promise對象。

api/api.js

// http對象,裡面有兩個對象方法分別是get和post
import http from './server'
// 獲取明星文章, 非詳情, 帶長度
export const getStarArticlesApi = p => http.post('/getStarArticles', p);
// 獲得組別數量及組別種類名
export const getAllGroupLengthApi = p => http.post('/getAllGroupLength', p);

  這種api寫法有兩個好處,第一其實和定義type是一個原因,可以避免出現api寫錯的情況,第二,當你定義的時候可以沒有必要確定你需要傳給後端的是一個什麼樣的數據類型,直接使用p就可以直接代替你想傳的所有值,方便你的初始定義。

  這個地方和TypeScript的思想其實是背道而馳的,因為TS希望你明確定義這個位置的詳細類型,而你使用的是JS,那麼就不需要對這裡進行限制。所以這裡在TS+react+redux的項目中是另一個不同的寫法,如果大家感興趣可以在評論區@我。對我講的可能不夠清晰的地方,也可以留下您的郵箱,我將這章的部分的程式碼發送給你,方便您進行試驗與測試。

  我是米卡