至今所有的例子,我们都用一个js文件完成。不得不说,这很有满足感。但这样的代码毫无阅读性,很难维护,也不易于团队合作。所以,本篇抛砖引玉,聊一下个人认为redux项目书写的最佳实践。
为此我重构了第二篇的todo-list如下:
- 清晰的文件夹结构,以功能归类
- 每个文件尽量只完成一件事
约定俗成,前端开发代码一般存在src
文件夹中,个人最偏爱的redux项目结构是
todo-list
├── README.md
├── node_modules
├── package.json
├── .gitignore
└── src
└── actions (文件夹)
└── reducers (文件夹)
└── components (文件夹)
└── configureStore.js
└── index.js
actions
└── index.js
reducers
├── todos.js
├── filter.js
└── index.js
components
├── AddTodo.js
├── Filter.js
├── List.js
└── index.js
- 也许你会疑惑为什么有这么多
index.js
文件?
这是一种习惯。index.js
代表一个文件夹下的“主文件”,所以每个文件夹下只有一个。值得一提的是,以index.js命名,引入(import)也会相对便利:
import reducer from './reducers/index'
//可以简写为
import reducer from './reducers'
显然components包含所有的react组件,这部分涉及到react-redux
的使用,我会在下一篇讨论。本篇主要讨论redux构架如何划分成多个文件。
复习一下redux构架的组成部分:
- state。App的初始状态
- actions。用户交互
- reducer。解释用户交互如何影响App的状态
- store。用
createStore
建立
我们的划分是:
1和4 => configureStore.js
2 => actions/index.js
一个文件
3 => reducers
文件夹
将原todo-list的代码:
import {createStore,combineReducer } from 'redux'
//初始state
const initialState = {
todos: [
{ id: 1, detail: "学习graphQL", completed: false },
{ id: 2, detail: "写博客", completed: false },
{ id: 3, detail: "本周的权力的游戏", completed: true }
],
filter: "all"
}
//actions
//添加一条todo:
const addTodo = detail => ({
type: "ADD_TODO",
payload: { detail }
})
//将todo的完成状态反转
const toggleTodo = id => ({
type: "TOGGLE_TODO",
payload: { id }
})
//修改todo-list的筛选器
const changeFilter = filter => ({
type: "CHANGE_FILTER",
payload: { filter }
})
//reducer
let nextId = 4 //1,2,3已经被使用了
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: nextId++,
detail: action.payload.detail,
completed: false
}
]
case "TOGGLE_TODO":
return state.map(t => {
if (t.id === action.payload.id) {
return { ...t, completed: !t.completed }
}
return t
})
default:
return state
}
}
const filter = (state = "all", action) => {
if (action.type === "CHANGE_FILTER") {
return action.payload.filter
}
return state
}
//合并reducer
const reducer = combineReducers({
todos,
filter
})
//创建store
const store = createStore(app, initialState)
重构如下:
// configureStore.js
import { createStore } from "redux"
import app from "./reducers"
//初始状态放于此,方便createStore使用
const initialState = {
todos: [
{ id: 1, detail: "学习graphQL", completed: false },
{ id: 2, detail: "写博客", completed: false },
{ id: 3, detail: "本周的权力的游戏", completed: true }
],
filter: "all"
}
const configureStore = () => createStore(app, initialState)
export default configureStore
- 之后随着中间件和localStorage等使用,store的建立不再是一行代码,所以单独分出此文件配置store。这也是我们使用一个可接收参数的
configureStore
函数作为默认导出的原因
// actions/index.js
//添加一条todo:
export const addTodo = detail => ({
type: "ADD_TODO",
payload: { detail }
})
//将todo的完成状态反转
export const toggleTodo = id => ({
type: "TOGGLE_TODO",
payload: { id }
})
//修改todo-list的筛选器
export const changeFilter = filter => ({
type: "CHANGE_FILTER",
payload: { filter }
})
- action的写法因人而异,不过个人觉得有必要建立一个actions.js文件用于集中保存所有的actions
- 浏览此文件就可以了解所有可能的用户交互以及它们需要的参数,这在大项目里是非常宝贵的
- 使用时只需
import { addTodo,toggleTodo } from '../actions'
即可准确引入此组件需要的actions
reducers
├── todos.js
├── filter.js
└── index.js
// reducers/index.js
import { combineReducers } from "redux"
import todos from "./todos"
import filter from "./filter"
const app = combineReducers({ todos, filter })
export default app
//选择器
export const getFilteredTodos = ({ todos, filter }) =>
todos.filter(t => {
if (filter === "all") {
return true
} else {
return (
(t.completed && filter === "completed") ||
(!t.completed && filter === "active")
)
}
})
// reducers/todos.js
let nextId = 4
const todos = (state = [], action) => {
switch (action.type) {
case "ADD_TODO":
return [
...state,
{
id: nextId++,
detail: action.payload.detail,
completed: false
}
];
case "TOGGLE_TODO":
return state.map(t => {
if (t.id === action.payload.id) {
return { ...t, completed: !t.completed };
}
return t;
})
default:
return state
}
}
export default todos
// reducers/filter.js
const filter = (state = "all", action) => {
if (action.type === "CHANGE_FILTER") {
return action.payload.filter
}
return state
}
export default filter
- 每类reducer写在单独的文件
- 在
index.js
里import所有的reducers,使用combineReducers
合并 - 在
index.js
里除了默认导出(export default)外,还有命名导出(export const ...), 这是Dan提出的一种规范,称为选择器
App里,各种组件往往需要显示各种结构复杂、条件各异的数据。就例如todo-list里,我们需要根据筛选项的不同(全部/完成/未完成)显示不同的列表,这个filteredList
并不直接存在于store里,而需要一定的计算,这类计算函数一般以get开头来取名,称为选择器。
那么选择题来了,选择器应该放在
- 使用此选择器的组件的
render
方法中 - 使用此选择器的组件的
connect
方法中(react-redux的方法) - 统一放在reducer中
答案:统一放在reducer中。
理由有二
- reducer是最清楚数据的结构,所以最清楚选择器写法的地方
- app的开发伴随着state的结构重写,以及它导致的选择器重写。我们不希望每次改写都花时间寻找“散落各处”的选择器,所以集中放在
reducers/index.js
中便是情理之中的选择
至此,redux项目的最佳实践的讨论进行的一大半。作为收尾,下面大家可以直接进入
- 第六篇: react-redux教程,以todo-list为例完成最佳实践的全部讨论
或者先回顾一下
亦或学习
- 第五篇: 如何写好reducer,这是事实是学习redux最大的难点之一。