Skip to content

Latest commit

 

History

History
276 lines (243 loc) · 7.62 KB

03.怎样写出整洁可扩展的redux项目.md

File metadata and controls

276 lines (243 loc) · 7.62 KB

怎样写出整洁可扩展的redux项目?

至今所有的例子,我们都用一个js文件完成。不得不说,这很有满足感。但这样的代码毫无阅读性,很难维护,也不易于团队合作。所以,本篇抛砖引玉,聊一下个人认为redux项目书写的最佳实践。

为此我重构了第二篇的todo-list如下:

重构后的todo-list Demo

整洁可扩展的代码在宏观上要做到

  1. 清晰的文件夹结构,以功能归类
  2. 每个文件尽量只完成一件事

文件夹结构

约定俗成,前端开发代码一般存在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构架的组成部分:

  1. state。App的初始状态
  2. actions。用户交互
  3. reducer。解释用户交互如何影响App的状态
  4. 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提出的一种规范,称为选择器

选择器(selectors)

App里,各种组件往往需要显示各种结构复杂、条件各异的数据。就例如todo-list里,我们需要根据筛选项的不同(全部/完成/未完成)显示不同的列表,这个filteredList并不直接存在于store里,而需要一定的计算,这类计算函数一般以get开头来取名,称为选择器
那么选择题来了,选择器应该放在

  1. 使用此选择器的组件的render方法中
  2. 使用此选择器的组件的connect方法中(react-redux的方法)
  3. 统一放在reducer中

答案:统一放在reducer中。
理由有二

  1. reducer是最清楚数据的结构,所以最清楚选择器写法的地方
  2. app的开发伴随着state的结构重写,以及它导致的选择器重写。我们不希望每次改写都花时间寻找“散落各处”的选择器,所以集中放在reducers/index.js中便是情理之中的选择

至此,redux项目的最佳实践的讨论进行的一大半。作为收尾,下面大家可以直接进入

或者先回顾一下

亦或学习