Redux 最佳實踐[譯]

力譜雲發表於2016-06-20

摘要

Redux 是 其他 flux 框架 推薦使用的 React 框架。當我開始寫這篇文章時,它還是 1.0.0 版本,當這篇文章釋出時,它已經是 3.0.0 了。

它的作者,Dan Abramov 已經發布一些很棒的 文件,但是他依然沒有完全指明如何在大規模專案中使用 Redux,所以人們開始問了 “有哪些大型專案使用了 Redux”. 好吧,希望這篇文章可以解決這些疑惑。

我們將會討論:

  • Redux 的所有技術棧

  • Redux 的各個模組都做了什麼

  • 如何劃分 Redux 專案結構

  • 如何處理 WebSocket 的非同步資料

正文

我應該有哪些知識儲備?

閱讀 Redux 官方文件。

閱讀了 Dan 的文章 Smart & Dumb Components

開發 Redux 專案需要使用哪些工具?

Redux 不僅僅是 Redux, 它是一堆相關東西的集合,其中有些已經發布到 v1.0.0,有些還在醞釀中。

你的工具包可能包含下面的絕大多數:

  • Webpack

使用它打包你的檔案,而不是使用 Browserify、Require 或者任何你掙扎使用的工具。為什麼?因為一部分 Redux 初始化示例 展示了它的熱載入能力,而且這些示例是使用 Webpack 構建的。

點選儲存就可以直接看到樣式的更新是如此方便,以至於我都不想再使用儲存->重新整理頁面->跳轉到指定頁面這樣繁瑣重複的方式了。

  • Babel

一部分原因是它讓你可以使用 ES6/7 的語法糖, 另一部份原因是熱載入現在是作為一個 Babel plugin 實現的。

  • React

雖然在這篇文章釋出時 0.13 版是穩定版本,但我們依然期待 0.14 版, 因為它能修復一些上下文相關的問題。

因為 ES6 提供了 classes 機制,所以 React 也 棄用 Mixins了。現在應該使用 高階元件 來代替–把你的 React 元件包裹在一個提供 上下文 的父元素中。Redux 充分利用了這一點。

  • Redux

這個沒什麼好說的。

  • React-Redux

嚴格來說,它與 Redux 無關,它是為 React 編寫的,提供了把 React 元件和 Redux Store 連線在一起的高階元件。

  • Middleware

你有兩個選擇:thunks 或者別的 promise 庫。無論哪個選擇,它都能讓你在 action creatores 中執行非同步程式碼成為可能。

  • Request Library

這正是上述非同步程式碼的原因。我使用 Axios,它基於 promise,因此可以和 promise 中介軟體很好的相容。

  • React-Router(-redux)

表面上看,使用路由就是更新導航欄並顯示對應的應用頁面。然而更底層的原因是它提供了一邏輯機制去拆分你的程式碼。

路由帶來的問題是,它給了你更多的 state,而這些 state 卻不屬於你的 store。Redux-Router 可以確保你的 state 被 Redux 管理。

如何使用 Redux 的不同部分?

我們都知道 Flux 是一個單向資料流框架,但即使這樣,我們如何使用它?

在應用中你需要:

  • 獲取應用的初始狀態

  • 根據狀態繪製內容

  • 處理 UI 互動

  • 處理 request 並且保持 state 與 store的同步

  • 更新和重繪內容

在一個不太規範的框架中,你可以隨意放置內容,可能在一個活著兩個不同的地方做了上述所有事情。

我按照以下的標準組織我的程式碼:

使用路由來確保你的元件擁有正確的資料

這是一個很好的方式,因為它劃分了資料集合。使用 Route 中的 onEnter 方法 來指定需要渲染的東西。你不必讓這個方法等到資料集合載入完畢,因為。。。

使用智慧元件來確保你的木偶元件可以渲染

你的智慧元件應該是配置在 Route 中的元件,你的智慧元件的 render 方法控制子元件的渲染資料:

render () {
  if (this.hasData()) {
    return this.renderComponents();
  } else {
    return this.renderLoadingScreen();
  }
}

智慧元件儘可能的做資料預處理,以使你的木偶元件足夠 “木偶”

比如說,當你傳遞一個處理控制程式碼給木偶元件時,帶上它需要的 id,這樣
木偶元件就不需要自己獲取 id 了:

renderComponents () {
  return <DumbComponent 
    onSelect={this.itemSelected.bind(this, this.props.item.id)}
  >;
}

使用木偶元件去渲染所有東西

不要放哪怕一個 <div> 到你的智慧元件中,任何時候,智慧元件都應該僅僅是木偶元件的組合。拆分你的關注點,不要在這裡寫一點東西。

使用智慧元件呼叫 actions creators

當一個木偶元件和使用者有互動時,它自己不應該處理任何邏輯--它應該僅僅呼叫從智慧元件中傳過來的處理函式,然後由這個函式去處理。

然後智慧元件採集必要的資料傳遞給 action creator。

在 ActionCreators 中轉換應用資料結構到 API 資料結構

你的 ActionCreators 負責在應用資料結構和 API 資料結構間轉換。這個操作是雙向的--發起請求,處理返回值。

因為 action 的輸出會被 reducer 處理,而 reducer 並不知道自己是被怎樣呼叫的,你可能發現有時候你不能僅僅返回 API 的呼叫結果--你需要補充它的附加欄位,比如:如果你的 action 是 PROJECT_UPDATE,你需要返回新的專案名和 id,而 API 僅僅返回 {savedAt: “<some date>”},你就需要這樣傳遞引數:

function updateProject(projectId, projectName) {
  request.put(`/project/${projectId}`, {projectName}).then(
    response => Object.assign(
      {projectId, projectName}, 
      response.data
    )
  );
}

使用 reducers 同步你的 state

有趣的是,一個 reducer 可以處理任何的 action。一個資料清理的場景是,當使用者登出時,清理 store 中的所有資料:

switch (action.type) {
   ...
   case USER_LOGOUT: 
     return {}
}

檔案結構

如何組織檔案結構是件複雜的事,因為它比處理一成不變的東西多了很多藝術性和個人風格。

我找到了 Redux 應用中的兩個分離點,然後我圍繞這兩個分離點組織檔案結構。

一個分離點是 資料。你的 actions 可以在任何地方被呼叫(雖然通常都是被智慧元件呼叫)。你的 reducers 和 actions 是繫結的。actions 可以組合在一起,根據模組構建你的應用:可能一部分是處理使用者登入和許可權,另一部分是使用者管理的專案。所有這些都有建立、查詢、更新和刪除,而這些都應該放在一起。

另一個分離點是 檢視。根據檢視你就可以佈局你的應用--不同頁面的路由,聚合資料和互動的智慧元件,渲染資料的木偶元件。

多個檢視可以呼叫同一個 action。比如,專案列表頁面可以讓你簡單的編輯專案名,而專案詳情頁面可以提供一個編輯專案名的表單。而這兩者都有不同分離的路由,不同的智慧元件,不同的木偶元件和不同的資料集。

所以,我這樣組織我的專案檔案:

public/
  index.html
  client/
    index.js
    modules/
      reducers.js
      users/
        constants.js
        actions/
          user_fetch.js
          user_login.js
          permissions_fetch.js
        reducers/
          index.js
          user.js
          permission.js
      projects/
    routes/
      login/
        index.js
        containers/
          login.js
        components/
          login.js
      logged_in/
      project_list/
      project_view/

modules 目錄負責處理和資料相關的檔案,不同模組的資料處理通過子目錄的方式劃分。這使得您未來可以把這些模組單獨打包到你的 npm 倉庫,它們之間沒有依賴。

每個 action 和 reducer 都有自己單獨的檔案。有的專案 試圖把一個模組中的所有內容都放倒一個檔案中。我個人反對在中大型專案中採用這種做法,當專案越來越大時,應該把東西拆成儘可能小的塊。

為了使不同的模組的 reducers 保持相似的結構,增加了 index.js 檔案,它匯出了該目錄中的所有 reducer,然後頂層的 reducers.js 引入所有模組的 reducers。這些單獨的 reducers 檔案都會用於生成 Redux store。

routes 目錄負責管理所有檢視相關的檔案,按不同的路由劃分子目錄。每個 route 目錄包涵三個部分:

  • 在 containers 目錄中的智慧元件

  • 在 components 目錄中的木偶元件

  • 包含 Route 的 index.js 檔案

同樣的,隨著路徑層級變深,會分解成更多的小元件。我推薦這種方式,因為它允許你僅僅在需要的時候例項化這些路由。而且意味著你的路由僅僅包含子其子目錄中的檔案,這樣感覺很好並且解偶了。

通過使用 onEnter 和 onLeave 方法,你的路由檔案同樣可以作為資料的關卡。在這裡你可以觸發 fetch action 來獲取元件需要的資料。這在你使用深層路由巢狀的時候很有用,比如,給定路由 /app/project/10/permission,你可以:

  • /app 中獲取當前使用者的登入資訊

  • /project 中獲取該使用者可見的專案

  • /10 中獲取專案 10 的詳細資訊

  • /permission 中獲取該使用者的許可權列表

當切換到另外一個路由 /app/project/11,你僅僅需要獲取更改的資料(/11 對應的資料),這時你就只需要一次對專案 11 的請求了:

import Projects from "./containers/projects";
import ProjectDetailRoute from "routes/project_detail";
export default class ProjectList {
  constructor () {
    this.path = "project";
    this.projectDetailRoute = new ProjectDetailRoute();
  }
  getChildRoutes (state, cb) {
    cb(null, [this.projectDetailRoute]);
  }
  getComponents (cb) {
    cb(null, ProjectTasks);
  }
  onEnter () {
    this.fetchProjects();
  }
  fetchProjects () {
    ...
  }
}

如何命名

Actions: <名詞>-<動詞>,比如 Project-Create,User-Login。依據是按照物件型別而不是動作型別分組。

Reducers: <名詞>。

如何處理第三方非同步資料

很明顯的這裡有條正確的流程(Action->Reducer->SmartContainer->DumbComponent)。但如何讓你的更改符合這個流程?

第三方非同步資料通常來自於 WebSocket。你可能僅僅想在應用的某些部分監聽它,比如登入時,或者某些頁面。而且,從 UI 到 actions 的處理流程是,使用者觸發了一個事件,木偶元件把事件傳播到智慧元件,然後觸發一個 action。

但在這種情況下,沒有木偶元件渲染內容,而由路由決定你何時接收資料,action 把資料注入到 redux。這個智慧元件不需要任何木偶元件,也應該獨立於其他智慧元件。

React-Route 很好的處理了這個問題:
Route 可以有多個元件:

getComponents () {
  cb(null, {view: ViewContainer, data: DataContainer};
}

該智慧元件可以這樣渲染:

render () {
  return <div>{this.props.view}{this.props.data}</div>
}

DataContainer 可以通過 componentDidUpdate 對 props 的更改作出響應,或者根據 componentWillUnmount 關閉連線。

總結

我已經連續兩週在寫這篇文章了,因為我總覺得還有些東西需要加進去。故事沒有結束,但我把它釋出出來以使 Redux 新手可以看到我對 Reactiflux 的探索。請評論和註釋這篇文章,我將在接下來的幾周內持續關注它。

作者資訊

原文作者: Will Becker
原文連結: https://medium.com/lexical-labs-engineering/redux-best-practices-64d59775802e#.1b8hgoju1
翻譯自MaxLeap團隊_UX成員:Henry Bai
力譜宿雲團隊首發:https://blog.maxleap.cn/archives/930

歡迎關注微信訂閱號:從移動到雲端
歡迎加入我們的MaxLeap活動QQ群:555973817,我們將不定期做技術分享活動。
若有轉載需要,請轉發時注意自帶作者資訊一欄並本自媒體公號:力譜宿雲,尊重原創作者及譯者的勞動成果~ 謝謝配合~

相關文章