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实现**[1]。
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
是一个对象,里面有两个对象方法分别是get
和post
,返回的都是一个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」的项目中是另一个不同的写法,如果大家感兴趣可以在评论区@我。对我讲的可能不够清晰的地方,也可以留下您的邮箱,我将这章的部分的代码发送给你,方便您进行试验与测试。
我是米卡
Reference
Redux的createStore实现: https://juejin.im/post/5eaee8e96fb9a04381514bab