关于React + Redux文件目录结构
初期
按照文件内容类型分:
-- project/
-- app/
-- assets/
-- icons/
-- components/
-- MyComponent/
-- index.jsx
-- style.scss
-- constants/
-- pages/
-- HomePage/
-- index.jsx
-- style.scss
-- redux/
-- model1/
-- model2/
-- store.js
-- styles/
-- global.scss
-- _variables.scss
-- index.js
-- .bablerc
-- webpack.config.js (如果用webpack的话)
-- package.json
-- ...
优点是直观省力,多人团队也较少放错地方。缺点是项目越长越大后会变得非常杂乱。
一种新的方案
-- project/
-- app/
-- feature1/
-- components/
-- redux/
-- constants.js
-- utils.js
-- feature2/
-- pages/
-- Page1/
-- components/
-- index.jsx
-- style.scss
-- store.js
-- index.js
-- uiComponents
-- utils/
-- .bablerc
-- webpack.config.js (如果用webpack的话)
-- package.json
-- ...
这个方案首先严格要求区分业务逻辑和非业务逻辑,然后业务逻辑还要区分业务线。这对每个队员的要求更高一些,分割文件的时候也更费力一些。
但好处也是显而易见的,这直接在文件结构的层次就要求大家减少代码间的耦合,让人在代码放哪多一些思考。另外在某个业务足够壮大需要独立出来时,也会比较省心省力
数据模型
如我在对Redux理解一问文中所说,将Redux最为一个前端本地数据库来对待的话,那么文件的组织和分割根据数据模型(Model)来分就自然而然了。
这里所谓的数据模型也就是MVC模式中的Model。在流行的RESTful API设计中通常按照某一数据模型来提供GET读取,POST创建,DELETE删除,PUT修改操作的接口,来最大化资源重复利用的可能性。
而在新近流行的GraphQL中更是让前端自己将多个数据模型组织成一个请求的返回数据… 咳咳, 扯远了。
只有读取操作
现在的应用只读不写的需求还是比较多的。只读需要处理的问题也比较少,可以简单分成如下结构:
-- model/
-- actions.js
-- reducer.js
-- constants.js
-- selectors.js
-- index.js
index.js中就只需要暴露action, selector, contants以及Typescript或者flow使用的state的类型,在UI部件中就可以一行import优雅地拿到所有数据模型相关的方法和常量了。
读写操作并存
读写操作并存的结构就相对复杂了。
在用户体验要求极高的今天,写操作意味着:
- 用户要看到写的进度
- 数据写入后,用户要看到界面上的更新
- 并且可能根据写入的结果决定下一步的操作
根据以上条件,我可以知道:
- 写操作需要跟读用同一个state。
- 写API请求成功后,API必须返回更新成功的完整数据模型。
原因之一是创建新数据的时候,只有写入数据库后这条数据才有ID,有了ID前端才有凭据找到用户刚刚操作的数据。
原因之二是修改数据的过程是:先读取现存的数据 > 用户修改 > 提交更新。
在用户修改的这个时间段内,很可能其他数据模型的更改会导致用户修改的这个数据模型中有一个字段发生了前端无法预测的改动,然后这些无法预测的变化又引发其他无法预测的蝴蝶效应(可能就是用户投诉,紧急修bug之类的连锁反应哈哈哈)。 - 因为上一条写完直接拿到了新数据的关系,既然写完需要反馈给用户他的更新结果,此时就不需要再一次读取同样的数据了,浪费API请求。
那么这个时候就要求写API成功时,reducer将返回的新数据更新到state中。使用React-redux的话,因为state的更新会触发新的渲染,我们的UI就自动更新为新数据了。
基于以上条件,一个读写操作并存的数据模型,我采取以下结构来干净地拆分文件:
-- model/
-- read/
-- actions.js
-- reducer.js
-- write/
-- actions.js
-- reducer.js
-- reducer.js
-- selectors.js
-- constants.js
只写
这种情况比较少见,只写的情况完全不需要用redux,所以这里掠过。
根据数据模型来组织Redux文件的优点
一般来说,一个UI部件只需要对1~2个数据模型的读写操作。这些读写操作就包含了:
- 用action来读写state
- 用selector从state中拿相关数据,或者直接从state中读取
- 而以上操作均需要绑定reducer
- 代码里可能还需要用一些state中使用的constants
- 如果使用Typescript或者flow之类类型定义的还需要使用相关的type
这样在Code Splitting代码拆分时,可减少无用代码一起被打包,减少客户端需要下载的包的大小。
尽量减少单个数据模型的深度
很多人容易将通同一业务线,哪怕是完全不同接口获取的不同数据绑定在一个reducer中。这样显得更加整洁容易管理。
这在多数情况下都不成问题,但注意要是使用比如react-persist之类的工具,还有在更新迭代却不想影响所有使用该数据模型的页面时就会造成一些麻烦。
所以我建议尽量在combineReducer时将数据模型铺铺平。
举个Blog的例子:
{
postList: { [key: string]: number[] },
post: {
[id: number]: {
title: string,
content: string,
comments: number[]
}
},
user: { [id: number]: User },
comment: { [id: number]: Comment },
...
};
而不要搞成:
{
posts: {
list: { [key: string]: Post[] },
post: { [key: id]: Post },
comment: { [id: number]: Comment }
},
user: { [id: number]: User },
...
}