1. 程式人生 > >React+Redux工程目錄結構,最佳實踐

React+Redux工程目錄結構,最佳實踐

參考

  1. Redux進階系列1: React+Redux專案結構最佳實踐
  2. 《深入淺出React和Redux》一書的第四章,P76,【4.2 程式碼檔案的組織方式】。

React+Redux 工程目錄結構組織

按角色型別組織

如果你用 MVC 框架開發過應用(無論是前端開發還是後端開發),應該知道 MVC 框架之下,通常有這樣一種程式碼組織方式:

controllers/
  todoController.js
  filterController.js
models/
  todoModel.js
  filterModel.js
views/
  todo.js
  todoItem.js
  filter.js

Controller、Model、View 分別代表三種模組角色。這種思想,對應到 Redux 應用,就有這種組織方式:

reducers/
  todoReducer.js
  filterReducer.js
actions/
  todoAction.js
  filterActions.js
components/
  todoList.js
  todoItem.js
  filter.js
containers/
  todoListContainer.js
  todoItemContainer.js

角色如下:

  • components 目錄包含所有的展示元件。
  • containers 目錄包含所有的容器元件。
  • actions 目錄包含所有 action 建構函式。
  • reducers 目錄包含所有 Redux 的 reducer。

充當component、container、action、reducer等不同角色的檔案,分別放在對應的多個資料夾下。

Redux 官網的 todomvc 示例採用了這種專案結構劃分,如果專案功能比較簡單,也還是可以採用這種方式的。

└─src
    │  index.js
    │
    ├─actions
    │      index.js
    │
    ├─components
    │      App.js
    │      Footer.js
    │      Header.js
    │      Link.js
    │      MainSection.js
    │      TodoItem.js
    │      TodoList.js
    │      TodoTextInput.js
    │
    ├─constants
    │      ActionTypes.js
    │      TodoFilters.js
    │
    ├─containers
    │      FilterLink.js
    │      Header.js
    │      MainSection.js
    │      VisibleTodoList.js
    │
    ├─reducers
    │      index.js
    │      todos.js
    │      visibilityFilter.js
    │
    └─selectors
            index.js

按角色型別組織的缺點

使用這種結構組織專案,每當增加一個新功能時,需要在containers和components資料夾下增加這個功能需要的元件,還需要在actions和reducers資料夾下,分別新增Redux管理這個功能使用到的action和reducer,如果action type是放在另外一個資料夾的話,還需要在這個資料夾下增加新的action type檔案。所以,開發一個功能時,你需要頻繁的切換路徑,修改不同的檔案。當專案逐漸變大時,這種專案結構是非常不方便的。

按照功能組織

在Redux框架下,我們可以嚴格按照模組化思想來開發我們的應用,以最大化的解耦應用,提高程式碼重用和可維護性。按照功能組織應用的程式碼,就是這種思想的應用。

一個功能模組對應一個資料夾,這個功能所用到的component、container、action、reducer等檔案,都存放在這個資料夾下。

拿Todo應用來說,兩個基本的功能就是TodoList和Filter,所以按功能組織就是這樣子:

todoList/
  components/
    componentA.js
    componentB.js
  containers.js
  actions.js
  actionTypes.js
  index.js
  reducer.js
filter/
  components/
    componentA.js
    componentB.js
  container.js
  actions.js
  actionTypes.js
  index.js
  reducer.js

參考了《深入淺出React和Redux》一書中給出的示例,同時結合自己的理解,有部分調整。

每個功能模組對應一個目錄,分別是todoList和filter,每個目錄下包含同樣的角色檔案:

  • components 目錄下包含功能模組中所有的展示元件。
  • container.js 定義容器元件。
  • actionTypes.js 定義action型別。
  • actions.js 定義action建構函式。
  • reducer.js 定義這個功能模組如果響應actions.js定義的動作。
  • index.js 把所有的角色匯入,然後統一匯出。

這種組織方式下,當你要修改某個模組時,只要關注對應的目錄即可,所有需要修改的程式碼檔案都能在這個目錄下找到。

需要注意的是,按功能組織下的每個模組,都有一個index.js,明確了模組對外的介面。

按照功能組織也有缺點

這種結構也有一個問題,Redux會將整個應用的狀態作為一個store來管理,不同的功能模組之間可以共享store中的部分狀態(專案越複雜,這種場景就會越多),於是當你在feature1的container中dispatch一個action,很可能會影響feature2的狀態,因為feature1和feature2共享了部分狀態,會響應相同的action。這種情況下,不同模組間的功能被耦合到了一起。

混合前面兩種方式–Ducks

Ducks: Redux Reducer Bundles,是一種新的Redux專案結構組織方式的提議。它提倡將相關聯的reducer、action types和action寫到一個檔案裡。本質上是以應用的狀態作為模組的劃分依據,而不是以介面功能作為劃分模組的依據。這樣的一個檔案(模組)如下:

// widget.js

// Actions
export const types = {
  const LOAD   : 'widget/LOAD',
  const CREATE : 'widget/CREATE',
  const UPDATE : 'widget/UPDATE',
  const REMOVE : 'widget/REMOVE'
}

const initialState = {
  widget: null,
  isLoading: false,
}

// Reducer
export default function reducer(state = initialState, action = {}) {
  switch (action.type) {
    types.LOAD: 
      //...
    types.CREATE:
      //...
    types.UPDATE:
      //...
    types.REMOVE:
      //...
    default: return state;
  }
}

// Action Creators
// 這樣定義而不是每個action都export避免了import時把所有action都列出來的繁瑣。
export const actions = {
  loadWidget: function() {
    return { type: types.LOAD };
  },
  createWidget: createWidget(widget) {
    return { type: types.CREATE, widget };
  },
  updateWidget: function(widget) {
    return { type: types.UPDATE, widget };
  },
  removeWidget: function(widget) {
    return { type: types.REMOVE, widget };
  }
}

由於使用Ducks提議的目錄結構,action creators和reducer定義在同一個檔案中,為了避免了引入額外的物件,import * 的匯入方式已經不推薦。為了使得匯入更加方便以及程式碼更加簡潔,比較好的辦法就是,把action creators和action types定義到一個名稱空間中(請看以上程式碼)。

這樣,我們在container中使用actions時,可以通過import { actions } from 'path/to/module.js' 引入,避免了import時把所有action都列出來的繁瑣。

同樣的,我們要在container中使用action types時,可以通過import { types } from 'path/to/module.js'引入。

整體的目錄結構如下:

components/  (應用級別的通用元件)
containers/  
  feature1/
    components/  (功能拆分出的專用元件)
    feature1.js  (容器元件)
    index.js     (feature1對外暴露的介面)
redux/
  index.js (combineReducers)
  module1.js (reducer, action types, actions creators)
  module2.js (reducer, action types, actions creators)
index.js

應用

應用Ducks工程目錄組織方式的思想,《React進階之路》一書的示例程式碼第9章,專案bbs-redux-reselect工程目錄結構如下:

└─src
    │  index.js
    │
    ├─components
    │  ├─Header
    │  │      index.js
    │  │      style.css
    │  │
    │  ├─Loading
    │  │      index.js
    │  │      style.css
    │  │
    │  └─ModalDialog
    │          index.js
    │          style.css
    │
    ├─containers
    │  ├─App
    │  │      index.js
    │  │
    │  ├─Home
    │  │      index.js
    │  │
    │  ├─Login
    │  │      index.js
    │  │      style.css
    │  │
    │  ├─Post
    │  │  │  index.js
    │  │  │  style.css
    │  │  │
    │  │  └─components
    │  │      ├─CommentList
    │  │      │      index.js
    │  │      │      style.css
    │  │      │
    │  │      ├─CommentsView
    │  │      │      index.js
    │  │      │      style.css
    │  │      │
    │  │      ├─PostEditor
    │  │      │      index.js
    │  │      │      style.css
    │  │      │
    │  │      └─PostView
    │  │              index.js
    │  │              style.css
    │  │
    │  └─PostList
    │      │  index.js
    │      │  style.css
    │      │
    │      └─components
    │          ├─PostItem
    │          │      index.js
    │          │      style.css
    │          │
    │          └─PostsView
    │                  index.js
    │
    ├─images
    │      like-default.png
    │      like.png
    │
    ├─redux
    │  │  configureStore.js
    │  │
    │  └─modules
    │          app.js
    │          auth.js
    │          comments.js
    │          index.js
    │          posts.js
    │          ui.js
    │          users.js
    │
    └─utils
            AsyncComponent.js
            connectRoute.js
            date.js
            request.js
            SHA1.js
            url.js