使用React + Redux + React-router構建可擴展的前端應用

如今是前端開發最好的時代,有太多很好的框架和工具幫你更好的實現複雜需求;同時又是最困難的時代,由於須要掌握太多的框架和工具。如何利用好各類框架來提升前端開發質量是你們都在探索的問題。本文就將介紹如何使用 React 及其相關技術,來進行實際前端項目的開發。由於主要介紹如何將技術用於實踐,因此但願讀者已經對相關概念已經有必定的瞭解。javascript

本文最初來源於筆者在 StuQ 的一次同名課程直播,如今加以整理成文,但願能對更多的人有所啓發。爲了固化這種實踐方式,當時還開發了一個名爲 Rekit 的工具,用於確保項目可以始終遵循這種實踐方式。如今工具也得到進一步完善,你們也能夠結合 Rekit 來理解文中提到的實踐方案。css

Paste_Image.png

其實不管使用什麼樣的技術,一個理想中的 Web 項目大概都須要考慮如下幾個方面:html

Paste_Image.png

  1. 易於開發:在功能開發時,無需關注複雜的技術架構,可以直觀的寫功能相關的代碼。前端

  2. 易於擴展:增長新功能時,無需對已有架構進行調整,新功能和已有功能具備很好的隔離性,並能很好的銜接。新功能的增長並不會帶來顯著的性能問題。java

  3. 易於維護:代碼直觀易讀易理解。即便是新加入的開發成員,也可以很快的理解技術架構和代碼邏輯。react

  4. 易於測試:代碼單元性好,可以儘可能使用純函數。無需或不多須要 mock 便可完成單元測試。webpack

  5. 易於構建:代碼和靜態資源結構符合主流模式,可以使用標準的構建工具進行構建。無需本身實現複雜的構建邏輯。git

這些方面並非互相獨立,而是互相依賴互相制約。當某個方面作到極致,其它點就會受到影響。舉例來講,寫一個計數器功能,用jQuery一個頁面內便可完成,可是易開發了,卻不易擴展。所以咱們一般都須要根據實際項目狀況在這些點之間作一個權衡,達到適合項目的最佳狀態。慶幸的是,如今的前端技術快速發展,不斷出現的新技術幫助咱們在各個方面都得到很大提高。github

本文將要介紹的就是如何利用 React + Redux + React-router 來構建可擴展的前端應用。這裏強調可擴展,由於傳統前端實現方案一般在面對複雜應用時經常力不從心,代碼結構容易混亂,性能問題難以解決。而可擴展則意味着可以從項目的初始階段就具備了支持複雜項目的能力。首先咱們看下涉及到的主要技術。web

React

React 相信你們已經很是熟悉,其組件化的思想和虛擬 DOM 的實現都是顛覆性的變革,從而讓前端開發能夠在新的方向上不斷提高。不管是 React-hot-loaderRedux 仍是 React-router,都正是由於充分利用了 React 的這些特性,纔可以提供如此強大的功能。筆者曾經寫過《深刻淺出React》 的系列文章,有須要的話能夠進一步閱讀。

Redux

Redux 是 JavaScript 程序狀態管理框架。儘管是一個通用型的框架,可是和 React 在一塊兒可以更好的工做,由於當狀態變化時,React 能夠不用關心變化的細節,由虛擬 DOM 機制完成優化過的UI更新邏輯。

Redux 也被認爲整個 React 生態圈最難掌握的技術之一。其 action,reducer 和各類中間件雖然將代碼邏輯充分隔離,即常說的 separation of concerns,但在必定程度上也給開發帶來了不便。這也是上面提到的,在易維護、易擴展、易測試上獲得了提高,那麼易開發則受到了影響。

React-router

即便對於一個簡單的應用,路由功能也是極其重要的。正如傳統 Web 程序用頁面來組織不一樣的功能模塊,由不一樣的 URL 來區分和導航,單頁應用使用 Router 來實現一樣的功能,只是在前端進行渲染而不是服務器端。React 應用的「標準」路由方案就是使用 React-router。

路由功能不只讓用戶更容易使用(例如刷新頁面後維持 UI),也可以在開發時讓咱們思考如何更好組織功能單元,這也是功能複雜以後的必然需求。因此即便一開始的需求很簡單,咱們也應該引入 React-router 幫助咱們以頁面爲單元進行功能的組織。

其它須要的技術

正如前面提到的,開發前端應用須要不少周邊技術,這進一步增長了前端開發的門檻,例如:

這些工具提升了前端開發的能力和效率,可是瞭解並配置它們卻並不是易事,而事實上這些工具和須要開發的功能並無直接的關係。使用工具來自動化這些配置是必然的發展方向,正如如今開發一個 C++ 應用,Visual Studio 會幫你完成全部的配置並搭建合適的項目結構,讓你專一於功能邏輯的開發。不管是本身實現,仍是利用第三方,咱們都應該爲本身的項目建立這樣的工具鏈。

簡單介紹了相關技術,下面咱們來看如何去構建可擴展的 Web 項目。

按功能(feature)來組織文件夾結構

不管是 Flux 仍是 Redux,提供的官方示例都是以技術邏輯來組織文件夾的,例如,下面是 Redux 的 Todo 示例應用的文件夾結構:

Paste_Image.png

雖然這種模式在技術上很清晰,在實際項目中卻有很大的缺點:

  1. 難以擴展。當應用功能增長,規模變大時,一個 components 文件夾下可能會有幾十上百個文件,組件間的關係極不直觀。

  2. 難以開發。在開發某個功能時,一般須要同時開發組件,action,reducer 和樣式。把它們分佈在不一樣文件夾下嚴重影響開發效率。尤爲是項目複雜以後,不一樣文件的切換會消耗大量時間。

所以,咱們使用按功能來組織文件夾的方式,即功能相關的代碼放到一個文件夾。例如,對於一個簡單論壇程序,可能包含 user,topic,comment 這麼幾個核心功能。

Paste_Image.png

每一個功能文件夾下包含本身的頁面,組件,樣式,action 和 reducer。

Paste_Image.png

這種文件夾結構在功能上而非技術上對代碼邏輯進行區分,使得應用具備更好的擴展性,當增長新的功能時,只需增長一個新的文件夾便可;刪除功能時同理。

使用頁面(Page)的概念

前面提到了路由是當今前端應用的不可缺乏的部分之一,那麼對應到組件級別,就是頁面組件。所以咱們在開發的過程當中,須要明肯定義頁面的概念:

  1. 一個頁面擁有本身的 URL 地址。頁面的展示和隱藏徹底由 React-router 進行控制。當建立一個頁面時,一般意味着在路由配置裏增長一條新的規則。這和傳統 Web 應用很是相似。

  2. 一個頁面對應 Redux 的容器組件的概念。頁面首先是一個標準的 React 組件,其次它經過 react-redux 封裝成容器組件從而具有和 Redux 交互的能力。

頁面是導航的基本模塊單元,同時也是同一功能相關 UI 的容器,這種符合傳統 Web 開發方式的概念有助於讓項目結構更容易理解。

每一個 action 一個獨立文件

使用 Redux 來管理狀態,就須要進行 action 和 reducer 的開發。在官方示例以及幾乎全部的教程中,全部的 action 都放在一個文件,而全部的 reducer 則放在另外的文件。這種作法易於理解可是不具有很好的可擴展性,並且當項目複雜後,action 文件和 reducer 文件都會變得很冗長,不易開發和維護。

所以咱們使用每一個 action 一個獨立文件的模式:每一個 Redux 的 action 和對應的 reducer 放在同一個文件。使用這個作法的另外一個緣由是咱們發現每次建立完 action 幾乎都須要馬上建立 reducer 對其進行處理。把它們放在同一個文件有利於開發效率和維護。

以開發一個計數器組件爲例:

Paste_Image.png

爲實現點擊「+」號增長1的功能,咱們首先須要建立一個類型爲 "COUNTER_PLUS_ONE" 的 action ,以後就馬上須要建立對應的 Reducer 來更新 store 的數據。官方示例的作法是分別在 actions.js 和 reducer.js 中分別加入相應的邏輯。而使用每一個 action 獨立文件的作法,則是建立一個名爲 counterPlusOne.js 的文件,加入以下代碼:

import {
  COUNTER_PLUS_ONE,
} from './constants';

export function counterPlusOne() {
  return {
    type: COUNTER_PLUS_ONE,
  };
}

export function reducer(state, action) {
  switch (action.type) {
    case COUNTER_PLUS_ONE:
      return {
        ...state,
        count: state.count + 1,
      };

    default:
      return state;
  }
}

按咱們的經驗,大部分的 reducer 都會對應到相應的 action,不多須要跨功能全局使用。所以,將它們放入一個文件是徹底合理的,有助於提升開發效率。須要注意的是,這裏定義的 reducer 並非標準的 Redux reducer,由於它沒有初始狀態(initial state)。它僅僅是被功能文件夾下的根 reducer 調用。注意這個 reducer 固定命名爲 "reducer",從而方便其被自動加載。

對於異步 action(一般是遠程 API 請求),則須要對錯誤信息進行處理,所以在這個文件中有多個標準 action 存在。例如以保存文章爲例,在 saveArticle.js 這個 action 文件中,同時存在 saveArticle 和 dismissSaveArticleError 這兩個 action。

如何處理跨功能的 action?

儘管不是很常見,可是有些 action 是可能被多個 reducer 處理的。例如,對於站內聊天功能,當收到一條新消息時:

  1. 若是聊天框開着,那麼直接顯示新消息。

  2. 不然,顯示一條通知提示有新的消息。

可見,NEW_MESSAGE 這個 action 類型須要被不一樣的 reducer 處理,從而可以在不一樣的 UI 組件作不一樣的展示。爲了處理這類 action,每一個功能文件夾下都有一個 reducer.js 文件,在裏面能夠處理跨功能的 action。

雖然不一樣 action 的 reducer 分佈在不一樣的文件中,但它們和功能相關的 root reducer 共同操做同一個狀態,即同一個 store 分支。所以 feature/reducer.js 具備以下的代碼結構:

import initialState from './initialState';
import { reducer as counterPlusOne } from './counterPlusOne';
import { reducer as counterMinusOne } from './counterMinusOne';
import { reducer as resetCounter } from './resetCounter';

const reducers = [
  counterPlusOne,
  counterMinusOne,
  resetCounter,
];

export default function reducer(state = initialState, action) {
  let newState;
  switch (action.type) {
    // Put global reducers here
    default:
      newState = state;
      break;
  }
  return reducers.reduce((s, r) => r(s, action), newState);
}

它負責引入不一樣 action 的 reducer,當有 action 過來時,遍歷全部的 reducer 並結合須要的全局 reducer 來實現對 store 的更新。全部功能相關的 root reducer 最終被組合到全局的 Redux root reducer 從而保證全局只有一個 store 的存在。

須要注意的是,每當建立一個新的 action 時,都須要在這個文件中註冊。由於其模式很是固定,咱們徹底可使用工具來自動註冊相應的代碼。Rekit 能夠幫助作到這一點:當建立 action 時,它會自動在 reducer.js 中加入相應的代碼,既減小了工做量,又能夠避免出錯。

使用單文件 action 的好處

使用這種方式,能夠帶來不少好處,好比:

  1. 易於開發:當建立 action 時,無需在多個文件中跳轉;

  2. 易於維護:由於每一個 action 在單獨的文件,所以每一個文件都很短小,經過文件名就能夠定位到相應的功能邏輯;

  3. 易於測試:每一個 action 均可以使用一個獨立的測試文件進行覆蓋,測試文件中也是同時包含對 action 和 reducer 的測試;

  4. 易於工具化:由於使用 Redux 的應用具備較爲複雜的技術結構,咱們可使用工具來自動化一些邏輯。如今咱們無需進行語法分析就能夠自動生成代碼。

  5. 易於靜態分析:全局的 action 和 reducer 一般意味着模塊間的依賴。這時咱們只要分析功能文件夾下的 reducer.js,便可以找到全部這些依賴。

React-router 的規則定義

一般來講,咱們會經過一個配置文件定義全部的路由規則。一樣的,這種方式不具備擴展性,當項目變複雜以後,規則定義表會變得冗長而複雜。既然咱們已經以功能爲單位進行文件夾的組織,咱們一樣能夠把功能相關的路由規則也放到對應文件夾下。所以,咱們能夠利用 React-router 的 JavaScript API 進行路由規則的定義,而不是用常見的 JSX 語法。

例如,對於一個簡單論壇程序,主題功能對應的路由定義就放在 features/topic/route.js 中,內容以下:

import {
  EditPage,
  ListPage,
  ViewPage,
} from './index';

export default {
  path: '',
  name: '',
  childRoutes: [
    { path: '', component: ListPage, name: 'Topic List', isIndex: true },
    { path: 'topic/add', component: EditPage, name: 'New Topic' },
    { path: 'topic/:topicId', component: ViewPage },
  ],
};

全部功能相關的路由定義都被全局的根路由配置自動加載,所以,路由加載器具備以下的代碼模式:

import topicRoute from '../features/topic/route';
import commentRoute from '../features/comment/route';

const routes = [{
  path: '/rekit-example',
  component: App,
  childRoutes: [
    topicRoute,
    commentRoute,
    { path: '*', name: 'Page not found', component: PageNotFound },
  ],
}];

可見,這個全局路由加載器負責加載全部 feature 的路由規則。相似 root reducer,這裏的代碼模式也是很是固定的,所以能夠藉助工具來維護這個文件。當使用 Rekit 建立頁面時,就會自動在此加入路由規則。

使用工具輔助開發

由上面的介紹能夠看到,開發一個 React 程序並不容易,即便一個簡單的功能,也須要大量的瑣碎的,但卻很是重要的代碼來確保一個良好的架構,從而讓應用易於擴展和維護,雖然這些周邊代碼和你須要的功能並無直接關係。

例如,對於一個論壇程序,須要一個列表界面展現最近發表的主題,爲了作這樣一個頁面,咱們一般都須要完成如下步驟:

  1. 建立一個名爲 TopicList 的 React 組件;

  2. 爲 TopicList 定義一條路由規則;

  3. 建立一個名爲 TopicList.css 的樣式文件,並在合適的位置引入;

  4. 使用 react-redux 將 TopicList 組件封裝成容器組件,從而使其可使用 Redux store;

  5. 建立4種不一樣的 action 類型:FETCH_BEGIN, FETCH_PENDING, FETCH_SUCCESS, FETCH_FAILURE,一般定義在 constants.js;

  6. 建立兩個 action:fetchTopicList 和 dismissFetchTopicListError;

  7. 在 action 文件中引入類型常量;

  8. 在 reducer 中建立4個 swtich case 來處理不一樣的 action 類型;

  9. 在 reducer 文件中引入類型常量;

  10. 建立組件的測試文件及其代碼結構;

  11. 建立 action 的測試文件及其代碼結構;

  12. 建立 reducer 的測試文件及其代碼結構。

天!在正式開始寫論壇邏輯的第一行代碼以前,居然須要作這麼多瑣碎的事情。當這樣的事情手動重複了屢次以後,咱們以爲應該有工具來自動化這樣的事情。爲此建立了 Rekit 工具包,能夠幫助自動生成這些文件結構和代碼。不一樣於其它的代碼生成器,Rekit 基於一個相對固定的文件和代碼結構,所以能夠作更多的事情,例如:

  1. 它知道在哪裏以及如何定義路由規則;

  2. 它知道如何生成 action 類型常量;

  3. 它知道如何根據 action 名字來生成類型常量;

  4. 它知道如何根據 action 類型來建立 reducer;

  5. 它知道如何建立有意義的測試案例。

藉助於精心維護的工具,咱們能夠沒必要關注技術細節,而只需專一於功能相關的代碼,提升了開發效率。不只如此,工具也能夠減小錯誤,並在代碼結構,命名,配置等方面維持高度一致性,讓代碼更加容易理解和維護。

Rekit 針對本文提出的 React + Redux 開發實踐提供了一套工具集,其自己也是可擴展的。你徹底能夠根據須要更改代碼模板,或者提供本身的工具,針對本身的項目特性提供便捷的工具來提升開發效率。

小結

本文主要介紹瞭如何使用 React,Redux 以及 React-router 來開發可擴展的 Web 應用。其核心思路有兩個,一是以功能(feature)爲單位組件文件夾結構;二是採用每一個 action 單獨文件的模式。這樣可以讓代碼更加模塊化,增長和刪除功能都不會對其它模塊產生太大影響。同時使用 React-router 來幫助實現頁面的概念,讓單頁應用(SPA)也擁有傳統 Web 應用的 URL 導航功能,進一步下降了功能模塊間的耦合行,讓應用結構更加清晰直觀。

爲了支持這樣的實踐,文中還介紹了 Rekit 工具集,不只能夠幫助建立和配置初始的項目模板,並且還提供了大量實用的工具幫助以文中提到的方式自動生成技術結構,提升了開發效率。更多的工具介紹能夠訪問其官網:http://rekit.js.org

相關文章
相關標籤/搜索