標籤:

如何規模化React應用

編譯自 How To Scale React Applications

我們最近發布了 React Boilerplate 3.0。在發布這一版本前,我們與數百位開發者進行了溝通,討論了他們構建和規模化(scale) web 應用的方式。下面我將分享一些我們從中學到的東西。

React Boilerplate 不是「又一個模板項目」,而是希望能為開發者們提供創業或打造產品的最佳基石。

在過去,規模化大多和伺服器系統有關。隨著使用的用戶越來越多,你必須確保能在伺服器集群中添加更多機器,資料庫能分拆到多台伺服器上。

但現在,由於富 web 應用的崛起,規模化也成了前端的重要話題!複雜應用的前端也需要能應付得了大量用戶、開發者和組件。開發者們在一開始就需要考慮這三個方面(用戶、開發者和組件)的規模化,否則後面會碰到問題。

容器和組件

了解有狀態(「容器」)和無狀態(「組件」)組件的區別,能極大地提升對大型應用的清晰了解。容器管理數據或者與狀態相連,通常不會有樣式。組件則有樣式,且不用負責任何數據或狀態管理。基本上,容器負責讓組件正常工作,組件負責外觀。

根據這一點,我們清楚地區分了可復用組件和數據管理中間層。現在你去編輯組件時,就不用擔心會搞混數據結構;編輯容器,也不用擔心弄亂樣式了。

代碼組織結構

開發者們一般會按照類型來組織代碼,比如使用 actions/,components/,containers/ 等文件夾。假設有一個 NavBar 容器,容器會管理與之相關的狀態,還有一個 toggleNav 動作來打開關閉它。下面就是按類型組織的代碼結構圖:

這種組織方式用來做樣例還可以,但如果你有成百上千個組件,用通過這種方式組織的代碼開發就會很困難。每添加一項功能,你就必須在包含眾多文件的多個文件夾中找到正確的文件。這很無聊,也很容易讓人感到疲勞。

通過長期追蹤我們的 Github 問題列表和多次嘗試後,我們找到了一種好得多的代碼結構組織解決方案:

那就是按照功能來組織代碼!即把和某個功能相關的所有文件放到同一個文件夾里。下面是重新組織的 NavBar 代碼:

這種方式非常有利於重命名、查找文件和替換,多人協作也不會導致任何衝突。需要注意的是,這種組織方式並不意味著 redux actions 和 reducers 只能用在這個組件里,它們可以也應該用於其他組件中!

樣式管理

除了結構性的考慮之外,由於 CSS 本身的兩個特點,在基於組件的架構中使用 CSS 很難:全局命名和繼承。

獨一無二的類名

假設在某個大型應用中存在這段 CSS

.header { /* … */ }.title { background-color: yellow;}

很快你就會意識到問題,title 太常用了。其他開發者也可能會寫出下列代碼:

.footer { /* … */ }.title { border-color: blue;}

這會產生命名衝突,讓你不得不在成百上千個文件中找到這段把一切事情弄糟的聲明。

幸運的是,我們有 CSS Modules。CSS Modules 的關鍵之處在於:把組件和組件的樣式文件放在同一個文件夾里。

這樣我們就不用擔心命名規範了,可以使用非常常見的名字:

.button { /* … */}

然後我們在組件中 import 或 require CSS 文件即可,並在 className 中使用

/* Button.jsx */var styles = require("./styles.css");<div className={styles.button}></div>

如果你現在去查看對應的 DOM 結構,你會看到<div class="MyApp__button__1co1k"></div>!CSS Modules 幫你保證了類名的「獨一無二」。

為每個組件重置屬性

在 CSS 中,特定的屬性會在 DOM 節點上下繼承。比如,如果父節點有 line-height,而子節點沒有,子節點就會自動繼承父節點的 line-height。

在基於組件的架構中,這可不是我們想要的。比如下面的代碼:

.header { line-height: 1.5em; /* … */}.footer { line-height: 1; /* … */}

假設我們在這兩個組件中渲染一個 Button,這個 Button 在這兩個組件里會有不同的外觀!這一點不僅適用於 line-height,也適用於其他能繼承的屬性。

在以前,我們可以用 Reset CSS、Normalize.css 和 sanitize.css 來重置樣式表。但如果我們想將這一理念落實到每一個組件上呢?

這就是 PostCSS 插件 PostCSS Auto Reset 的用途!它會對每個組件進行樣式重置,將所有能繼承的 CSS 屬性設置成默認值,從而覆蓋繼承。

數據拉取

基於組件的架構面臨的第二大問題就是數據拉取。對於大部分 action 來說,將 action 和組件放在一起很合理,但數據拉取是全局 action,不和某個組件相連!

目前大多數開發者使用 Redux Thunk 來處理 Redux 中的數據拉取。標準的 thunked action 如下:

/* actions.js */function fetchData() { return function thunk(dispatch) { // 非同步載入 fetch("https://someurl.com/somendpoint", function callback(data) { // 將數據加入 store dispatch(dataLoaded(data)); }); }}

這種在 action 中拉取數據的方法很棒,但存在兩個問題:很難測試這些函數,以及在 action 中包含數據拉取看起來不太對。

Redux 的一大好處就是 pure action creator 很容易測試。而在 action 中加入了 thunk 的數據拉取操作之後,你必須兩次調用 action,模擬 dispatch 函數等。

最近,redux-saga 在 React 世界引起了廣泛關注。redux-saga 利用了 ES6 的 Generator 函數,讓非同步代碼看起來就像同步代碼一樣,而且讓非同步流非常容易測試。redux-saga 給人的感覺是一個單獨處理所有非同步事務的獨立線程,不會干擾應用的其他方面!

下面是 redux-saga 的一個例子:

/* sagas.js */import { call, take, put } from "redux-saga/effects";// 函數後面的星號表示這是 generator 函數function* fetchData() { // yield:在這個非同步函數完成前一直等待 yield take(FETCH_DATA); // 然後從伺服器拉取數據,重新通過 yield 等待 var data = yield call(fetch, "https://someurl.com/someendpoint"); // 當數據完成載入後,dispatch dataLoaded action. put(dataLoaded(data));}

上面的代碼讀起來就像小說一樣,避免了回調地獄,而且非常容易測試。為什麼?這是因為 redux-saga 導出的這些「作用」(effect)無需完成即可進行測試。

我們在文件頂部 import 的這些作用都是 handler,可以讓我們輕鬆與 redux 交互:

  • put() dispatches 一個 action

  • take() 暫停 saga 直到 action 發生

  • select() 獲取部分 redux 狀態(有點像 mapStateToProps)

  • call() 調用傳入的第一個位置的函數,並將其他參數作為被調用函數的參數

為什麼這些作用有用?讓我們看看下面的測試:

/* sagas.test.js */var sagaGenerator = fetchData();describe("fetchData saga", function() { // 測試當 action 被 dispatch 後,saga 啟動 // 即便無需真正模擬 dispatch 發生了 it("should wait for the FETCH_DATA action", function() { expect(sagaGenerator.next()).to.equal(take(FETCH_DATA)); }); // 測試 saga 用某個 URL 做參數調用了 fetch 函數 // 即便無需真正模擬 fetch 函數或使用 API 乃至聯網! it("should fetch the data from the server", function() { expect(sagaGenerator.next()).to.equal(call(fetch, "https://someurl.com/someendpoint")); }); // 測試 saga dispatch 了一個 action // 而無需主應用運行! it("should dispatch the dataLoaded action when the data has loaded", function() { expect(sagaGenerator.next()).to.equal(put(dataLoaded())); });});

只有 generator.next() 被調用時,generator 函數才會繼續,直到遇到下一個 yield 關鍵字!另外,我們也把測試文件和組件放在了一起。

用 redux-saga 來做膠水中間件

我們的組件現在真正解耦了。它們不關心其他組件的樣式或邏輯;它們只關心自己的事情(絕大多數情況下如此)。

假設有一個 Clock 和一個 Timer 組件。當 Clock 中的按被按下時,我們想要啟動 Timer;當 Timer 中的停止按鈕被按下時,我們想要在 Clock 中顯示時間。

你也許會這麼做:

/* Clock.jsx */import { startTimer } from "../Timer/actions";class Clock extends React.Component { render() { return ( /* … */ <button onClick={this.props.dispatch(startTimer())} /> /* … */ ); }}/* Timer.jsx */import { showTime } from "../Clock/actions";class Timer extends React.Component { render() { return ( /* … */ <button onClick={this.props.dispatch(showTime(currentTime))} /> /* … */ ); }}

但這麼做的話,你無法單獨使用這兩個組件,復用它們幾乎不可能!

不過,我們可以用 redux-saga 來做這兩個解耦組件的「膠水中間件」。取決於應用類型,我們可以通過聽取特定 action,來以不同方式作出反應,從而讓組件真正變得可復用。

先修改兩個組件:

/* Clock.jsx */import { startButtonClicked } from "../Clock/actions";class Clock extends React.Component { /* … */ <button onClick={this.props.dispatch(startButtonClicked())} /> /* … */}/* Timer.jsx */import { stopButtonClicked } from "../Timer/actions";class Timer extends React.Component { /* … */ <button onClick={this.props.dispatch(stopButtonClicked(currentTime))} /> /* … */}

注意兩個組件現在只關心自己,只 import 了自己的 action !

下面用一個 saga 來把這兩個解耦的組件連接到一起:

/* sagas.js */import { call, take, put, select } from "redux-saga/effects";import { showTime } from "../Clock/actions";import { START_BUTTON_CLICKED } from "../Clock/constants";import { startTimer } from "../Timer/actions";import { STOP_BUTTON_CLICKED } from "../Timer/constants";function* clockAndTimer() { // 等待 Clock 的 startButtonClicked action 被 dispatch yield take(START_BUTTON_CLICKED); // dispatch 後,啟動 timer. put(startTimer()); // 等待 Timer 的 stopButtonClick action 被 dispatch yield take(STOP_BUTTON_CLICKED); // 從全局狀態獲取 Timer 的當前時間 var currentTime = select(function (state) { return state.timer.currentTime }); // 在 Clock 中顯示時間 put(showTime(currentTime));}

太美了!

總結

  • 容器和組件的區別

  • 按功能組織代碼文件

  • 使用 CSS modules 和 PostCSS Auto Reset

  • 使用 redux-saga 來:

  1. 獲得可讀且可測試的非同步流

  2. 將解耦組件連接到一起

推薦閱讀:

redux middleware 詳解
React+AntD後台管理系統解決方案(補)
【React/Redux/Router/Immutable】React最佳實踐的正確食用姿勢
如何在非 React 項目中使用 Redux
揭秘 React 狀態管理

TAG:React | Redux |