组织文件

组织文件,说的是文件(夹)应该放哪,哪些文件(夹)应该放在一起,和收拾屋子是一个道理。但是在开始之前,我要先说说应该如何限制你的文件大小。

文件尺寸

常常在接手别人的工作时,发现要修改的文件里的代码 1500 行起,这样大的脚本文件在维护和复用起来的时候是非常困难的。令人费解的是这种问题发生在 React 这种天生支持模块化的技术栈环境中,不免让人(对写代码的人的这种不计后果)感到愤怒

关于如何限定文件尺寸大小,我想用另一个问题来回答这个问题:

你觉得是一个 1500 行代码的文件便于维护,还是 3 个 500 行代码的文件便于维护,还是 5 个300 行代码的文件便于维护?

这个问题很容易想通,当你把这个问题想通了,这个问题的答案自然也有了

你可能会问了,如果我写的代码行数真的不得不超过了 1500 行怎么办?这个时候千万千万……不要怀疑业务的复杂度,要怀疑你的技术水平:没有 750 行代码解决不了的问题,如果有,就两个 750 行。同理,如果当你代码只有 750 行时,你要考虑的是,是否能把它拆成两个 375 行的代码解决问题

如何组织文件

在几乎所有的 redux 教程中,你看到的关于组织文件的方式都是按照文件类型进行分类的,比如所有的 reducer 都放在 reducers 文件夹中,所有的组件都放在 components 文件夹中。这种分类方式似乎上看去再天经地义不过了。例如:

components
|-productBox.js
|-productButtons.js
actions
|-productActions.js
reducers
|-productReducers.js

但我这里要告诉你,这样的分类仅仅适用于教程里的小型 webapp。一旦当应用的体量变大的时候,这种分类方式就变得不适用了

在 redux 体系中,一个文件的修改往往会牵动其他多个文件的修改。例如 actionTypes.js 文件中新增了某个动作类型,那么对应 action 文件,reducer 文件,以及涉及的 component 都要发生修改。那么在实际操作中,无论是在现在的新增还是在将来的修改中,你不得不跳转至不同的文件夹然后通过滚动超长的文件列表找到他们进行修改。但事实的情况是,这些看似不同的类型的文件都是为同一个功能服务的。并且我们在修改代码时,也都是以功能为单位的,所以我的建议,同时也是大部分人的建议是,按照功能 (feature) 将这些不同类型的文件组织在一起。那么上面的组织结构应该修改为:

product
|-productBox.js
|-productButtons.js
|-productActions.js
|-productReducers.js

“鸭子”文件组织法

​ “鸭子”文件组织法 是一个关于如何组织 redux 文件结构的开源倡议,它的理由和主张非常简单:

I have been keeping these in separate files and even separate folders, however 95% of the time, it's only one reducer/actions pair that ever needs their associated actions.

To me, it makes more sense for these pieces to be bundled together in an isolated module that is self contained, and can even be packaged easily into a library.

简单来说作者认为大多数情况下 reducer 和 action 是一一对应的关系,与其把他们独立为不同的文件,不如把它们打包到一个模块(文件)里,比如看它给的官方实例:

// widgets.js// Actions
const LOAD   = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
​
// Reducer
export default function reducer(state = {}, action = {}) {
  switch (action.type) {
    // do reducer stuff
    default: return state;
  }
}
​
// Action Creators
export function loadWidgets() {
  return { type: LOAD };
}
​
export function createWidget(widget) {
  return { type: CREATE, widget };
}
​
export function updateWidget(widget) {
  return { type: UPDATE, widget };
}
​
export function removeWidget(widget) {
  return { type: REMOVE, widget };
}
​
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
  return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}

在同一个文件中你同时看到 types, actions, reducer, creators

当然倡议者还给出了一些其他的规则。以及基于这个倡议诞生了很多相关类库用于实现和协助文件的组织。

“鸭子”方法是一个好的提倡,它与我们之前提出的按照功能组织文件的方法有异曲同工之妙。但是它仍然有局限性,例如在实际的 redux 文件结构中还会存在 selector 模块、saga 模块等,如果都存放在同一个文件中依然会造成维护的不便。所以这里我们可以根据自己的需求对“鸭子”做一些改进。

比如说我个人习惯把 action、reducer 等内容独立为文件。如果有需要的话甚至可以拆分为不同的文件夹进行保存。如果确实拆分为了多个文件,那么务必保证在每个类别的文件下都有 index.js,用于对外统一导出该文件夹中的内容,比如:

Product
|-index.js
|-Types
    |-index.js
    |-Add.js
    |-Remove.js
    |-Query.js
|-Reducers

那么Types 文件夹中的 index.js 的内容应该类似于:

import * as addTypes from './Add
import * as removeTypes from './Remove
import * as queryTypes from './Query
​
export default {
  addTypes,
  removeTypes,
  queryTypes
}

而整个 Product 文件夹中的 index.js 的内容类似于

import types from './types'
import actions from './actions'
import reducers from './reducers'
import sagas from './sagas'export default {types, actions, reducers, sagas}

当然以上仅供参考

最后不知道你有没有发现,组件并没有和其他文件内容按照鸭子法组织在一起,因为对于组件来说,大部分不会只隶属于某个业务,一个组件会共享多个业务的数据。所以不宜把它们放到任意一个鸭子文件夹中

Redux 组件的命名

我个人习惯遵从这篇文章 Redux Best Practices 的命名规范:

  • action type 常量使用大写的名词加动词的规则命名:_。这样便于将处理相同实体的动作都归纳到一起,一目了然。比如TODO_ADD
  • action creator 使用传统的驼峰命名法,并且以动词开头,后接名词:。因为 action creator 通常都是函数并且是以函数的形式调用。比如 addTodo,调用时 dispatch(addTodo('Hello World'))
  • selector 以 get 开头,并后接名词:get

参考资料

​- Three Rules For Structuring (Redux) Applications​ ​- A Better File Structure For React/Redux Applications​ ​- erikras/ducks-modular-redux​ ​- Scaling your Redux App with ducks​ ​- My journey toward a maintainable project structure for React/Redux​ ​- Domain directory structure for React apps: why it’s worth trying​ ​- Redux Best Practices​

results matching ""

    No results matching ""