React應用架構設計指南

NO IMAGE
1 Star2 Stars3 Stars4 Stars5 Stars 給文章打分!
Loading...

在上一篇我們介紹了Webpack自動化構建React應用,我們的本地開發伺服器可以較好的支援我們編寫React應用,並且支援程式碼熱更新。本節將開始詳細分析如何搭建一個React應用架構。

完整專案程式碼見github

個人部落格

前言

現在已經有很多腳手架工具,如create-react-app,支援一鍵建立一個React應用專案結構,很方便,但是享受方便的同時,也失去了對專案架構及技術棧完整學習的機會,而且通常腳手架建立的應用技術架構並不能完全滿足我們的業務需求,需要我們自己修改,完善,所以如果希望對專案架構有更深掌控,最好還是從0到1理解一個專案。

專案結構與技術棧

我們這次的實踐不準備使用任何腳手架,所以我們需要自己建立每一個檔案,引入每一個技術和三方庫,最終形成完整的應用,包括我們選擇的完整技術棧。

第一步,當然是建立目錄,我們在上一篇已經弄好,如果你還沒有程式碼,可以從Github獲取:

git clone https://github.com/codingplayboy/react-blog.git
cd react-blog

生成專案結構如下圖:

React專案初始結構

src

然後在建立redux store時將其作為redux強化器傳入createStore

在開發環境下獲取redux-devtools提供的拓展組合函式;
建立store時使用拓展組合函式組合redux中介軟體和增強器,redux-dev-tools便獲得了應用redux的相關資訊;

Reactotron

Reactotron是一款跨平臺除錯React及React Native應用的桌面應用,能動態實時監測並輸出React應用等redux,action,saga非同步請求等資訊,如圖:

Reactotron

首先安裝:

yarn add --dev reactotron-react-js

然後初始化Reactotron相關配置:

import Reactotron from 'reactotron-react-js';
import { reactotronRedux as reduxPlugin } from 'reactotron-redux';
import sagaPlugin from 'reactotron-redux-saga';
if (Config.useReactotron) {
// refer to https://github.com/infinitered/reactotron for more options!
Reactotron
.configure({ name: 'React Blog' })
.use(reduxPlugin({ onRestore: Immutable }))
.use(sagaPlugin())
.connect();
// Let's clear Reactotron on every time we load the app
Reactotron.clear();
// Totally hacky, but this allows you to not both importing reactotron-react-js
// on every file.  This is just DEV mode, so no big deal.
console.tron = Reactotron;
}

然後啟使用console.tron.overlay

至此就可以使用Reactotron客戶端捕獲應用中發起的所有的redux和action了。

元件劃分

React元件化開發原則是元件負責渲染UI,元件不同狀態對應不同UI,通常遵循以下元件設計思路:

佈局元件:僅僅涉及應用UI介面結構的元件,不涉及任何業務邏輯,資料請求及操作;
容器元件:負責獲取資料,處理業務邏輯,通常在render()函式內返回展示型元件;
展示型元件:負責應用的介面UI展示;
UI元件:指抽象出的可重用的UI獨立元件,通常是無狀態元件;

展示型元件容器元件
目標UI展示 (HTML結構和樣式)業務邏輯(獲取資料,更新狀態)
感知Redux
資料來源props訂閱Redux store
變更資料呼叫props傳遞的回撥函式Dispatch Redux actions
可重用獨立性強業務耦合度高

Redux

現在的任何大型web應用如果少了狀態管理容器,那這個應用就缺少了時代特徵,可選的庫諸如mobx,redux等,實際上大同小異,各取所需,以redux為例,redux是最常用的React應用狀態容器庫,對於React Native應用也適用。

Redux是一個JavaScript應用的可預測狀態管理容器,它不依賴於具體框架或類庫,所以它在多平臺的應用開發中有著一致的開發方式和效率,另外它還能幫我們輕鬆的實現時間旅行,即action的回放。

redux-flow

資料單一來源原則:使用Redux作為應用狀態管理容器,統一管理應用的狀態樹,它推從資料單一可信來源原則,所有資料都來自redux store,所有的資料更新也都由redux處理;
redux store狀態樹:redux集中管理應用狀態,組織管理形式就好比DOM樹和React元件樹一樣,以樹的形式組織,簡單高效;
redux和store:redux是一種Flux的實現方案,所以建立了store一詞,它類似於商店,集中管理應用狀態,支援將每一個釋出的action分發至所有reducer;
action:以物件資料格式存在,通常至少有type和payload屬性,它是對redux中定義的任務的描述;
reducer:通常是以函式形式存在,接收state(應用區域性狀態)和action物件兩個引數,根據action.type(action型別)執行不同的任務,遵循函數語言程式設計思想;
dispatch:store提供的分發action的功能方法,傳遞一個action物件引數;
createStore:建立store的方法,接收reducer,初始應用狀態,redux中介軟體和增強器,初始化store,開始監聽action;

中介軟體(Redux Middleware)

Redux中介軟體,和Node中介軟體一樣,它可以在action分發至任務處理reducer之前做一些額外工作,dispatch釋出的action將依次傳遞給所有中介軟體,最終到達reducer,所以我們使用中介軟體可以拓展諸如記錄日誌,新增監控,切換路由等功能,所以中介軟體本質上只是拓展了store.dispatch

最簡單的例子程式碼如上,新函式接收redux的createStore方法和建立store需要的引數,然後在函式內部儲存store物件上某方法的引用,重新實現該方法,在裡面處理完增強邏輯後呼叫原始方法,保證原始功能正常執行,這樣就增強了store的dispatch方法。

可以看到,增強器完全能實現中介軟體的功能,其實,中介軟體就是以增強器方式實現的,它提供的compose

react-redux庫提供Provider

redux與Immutable

redux預設提供了combineReducers

如上表明,原始型別reducer接受的state引數應該是一個原生JavaScript物件,我們需要對combineReducers

如上程式碼,可以看見我們傳入的initialState

這裡預設使用Immutable.fromJS()方法狀態樹節點物件轉化為Immutable結構,並且更新state時使用Immutable方法state.merge()

很簡單,但是所有的路由結構都需要在渲染應用前,統一定義,層層巢狀;而且如果要實現非同步按需載入還需要在這裡對路由配置物件進行修改,使用getComponent

相比之前版本,減少了配置化的痕跡,更凸顯了元件化的組織方式,而且在渲染元件時才實現該部分路由,而如果期望按需載入該元件,則可以通過封裝實現一個支援非同步載入元件的高階元件,將經過高階元件處理後返回的元件傳入<Route>

Redux整合

在使用Redux以後,需要遵循redux的原則:單一可信資料來源,即所有資料來源都只能是reudx store,react路由狀態也不應例外,所以需要將路由state與store state連線。

react-router-redux

連線React Router與Redux,需要使用react-router-redux

然後,在建立store時,需要實現如下配置:

建立一個history物件,對於web應用,我們選擇browserHisotry,對應需要從history/createBrowserHistory

在渲染根元件時,我們抽象出兩個元件:

初始化渲染根元件,掛載至DOM的根元件,由<Provider>

上面的<Routes>

首先使用react-router-redux

這個reducer所做的只是將App導航路由狀態合併入store。

redux持久化

我們知道瀏覽器預設有資源的快取功能並且提供本地持久化儲存方式如localStorage,indexDb,webSQL等,通常可以將某些資料儲存在本地,在一定週期內,當使用者再次訪問時,直接從本地恢復資料,可以極大提高應用啟動速度,使用者體驗更有優勢,我們可以使用localStorage儲存一些資料,如果是較大量資料儲存可以使用webSQL。

另外不同於以往的直接儲存資料,啟動應用時本地讀取然後恢復資料,對於redux應用而言,如果只是儲存資料,那麼我們就得為每一個reducer拓展,當再次啟動應用時去讀取持久化的資料,這是比較繁瑣而且低效的方式,是否可以嘗試儲存reducer key,然後根據key恢復對應的持久化資料,首先註冊Rehydrate reducer,當觸發action時根據其reducer key恢復資料,然後只需要在應用啟動時分發action,這也很容易抽象成可配置的拓展服務,實際上三方庫redux-persist已經為我們做好了這一切。

redux-persist

要實現redux的持久化,包括redux store的本地持久化儲存及恢復啟動兩個過程,如果完全自己編寫實現,程式碼量比較複雜,可以使用開源庫redux-persist

持久化store

如下在建立store時會呼叫persistStore相關服務-RehydrationServices.updateReducers()

該方法內實現了store的持久化儲存:

// Check to ensure latest reducer version
storage.getItem('reducerVersion').then((localVersion) => {
if (localVersion !== reducerVersion) {
// 清空 store
persistStore(store, null, startApp).purge();
storage.setItem('reducerVersion', reducerVersion);
} else {
persistStore(store, null, startApp);
}
}).catch(() => {
persistStore(store, null, startApp);
storage.setItem('reducerVersion', reducerVersion);
})

會在localStorage儲存一個reducer版本號,這個是在應用配置檔案中可以配置,首次執行持久化時儲存該版本號及store,若reducer版本號變更則清空原來儲存的store,否則傳入store給持久化方法persistStore

該方法主要實現store的持久化以及分發rehydration action :

訂閱 redux store,當其發生變化時觸發store儲存操作;
從指定的StorageEngine(如localStorage)中獲取資料,進行轉換,然後通過分發 REHYDRATE action,觸發 REHYDRATE 過程;

接收引數主要如下:

store: 持久化的store;
config:配置物件

storage:一個 持久化引擎,例如 LocalStorage 和 AsyncStorage;
transforms: 在 rehydration 和 storage 階段被呼叫的轉換器;
blacklist: 黑名單陣列,指定持久化忽略的 reducers 的 key;

callback:ehydration 操作結束後的回撥;

恢復啟動

和persisStore一樣,依然是在建立redux store時初始化註冊rehydrate拓展:

// add the autoRehydrate enhancer
if (ReduxPersist.active) {
enhancers.push(autoRehydrate());
}

該方法實現的功能很簡單,即使用 持久化的資料恢復(rehydrate) store 中資料,它其實是註冊了一個autoRehydarte reducer,會接收前文persistStore方法分發的rehydrate action,然後合併state。

當然,autoRehydrate不是必須的,我們可以自定義恢復store方式:

import {REHYDRATE} from 'redux-persist/constants';
//...
case REHYDRATE:
const incoming = action.payload.reducer
if (incoming) {
return {
...state,
...incoming
}
}
return state;

版本更新

需要注意的是redux-persist庫已經發布到v5.x,而本文介紹的以v5.x為例,v4.x參考此處,新版本有一些更新,可以選擇性決定使用哪個版本,詳細請點選檢視

持久化與Immutable

前面已經提到Redux與Immutable的整合,上文使用的redux -persist預設也只能處理原生JavaScript物件的redux store state,所以需要拓展以相容Immutable。

redux-persist-immutable

使用redux-persist-immutable庫可以很容易實現相容,所做的僅僅是使用其提供的persistStore

transform

我們知道持久化store時,針對的最好是原生JavaScript物件,因為通常Immutable結構資料有很多輔助資訊,不易於儲存,所以需要定義持久化及恢復資料時的轉換操作:

import R from 'ramda';
import Immutable, { Iterable } from 'immutable';
// change this Immutable object into a JS object
const convertToJs = (state) => state.toJS();
// optionally convert this object into a JS object if it is Immutable
const fromImmutable = R.when(Iterable.isIterable, convertToJs);
// convert this JS object into an Immutable object
const toImmutable = (raw) => Immutable.fromJS(raw);
// the transform interface that redux-persist is expecting
export default {
out: (state) => {
return toImmutable(state);
},
in: (raw) => {
return fromImmutable(raw);
}
};

如上,輸出物件中的in和out分別對應持久化及恢復資料時的轉換操作,實現的只是使用fromJS()

Immutable

在專案中引入Immutable以後,需要儘量保證以下幾點:

redux store整個state樹的統一Immutable化;
redux持久化對Immutable資料的相容;
React路由相容Immutable;

關於Immutable及Redux,Reselect等的實踐考驗檢視之前寫的一篇文章:Immutable.js與React,Redux及reselect的實踐

Immutable與React路由

前面兩點已經在前面兩節闡述過,第三點react-router相容Immutable,其實就是使應用路由狀態相容Immutable,在React路由一節已經介紹如何將React路由狀態連線至Redux store,但是如果應用使用了Immutable庫,則還需要額外處理,將react-router state轉換為Immutable格式,routeReducer不能處理Immutable,我們需要自定義一個新的RouterReducer:

import Immutable from 'immutable';
import { LOCATION_CHANGE } from 'react-router-redux';
const initialState = Immutable.fromJS({
location: null
});
export default (state = initialState, action) => {
if (action.type === LOCATION_CHANGE) {
return state.set('location', action.payload);
}
return state;
};

將預設初始路由狀態轉換為Immutable,並且路由變更時使用Immutable API操作state。

seamless-Immutable

當引入Immutable.js後,對應用狀態資料結構的使用API就得遵循Immutable API,而不能再使用原生JavaScript物件,陣列等的操作API了,諸如,陣列解構([a, b] = [b, c]),物件拓展符(…)等,存在一些問題:

Immutable資料輔助節點較多,資料較大:
必須使用Immutable語法,和JavaScript語法有差異,不能很好的相容;
和Redux,react-router等JavaScript庫寫協作時,需要引入額外的相容處理庫;

針對這些問題,社群有了seamless-immutable

saga分流

在專案中通常會有很多並列模組,每個模組的saga流也應該是並列的,需要以多分支形式並列,redux-saga提供的fork

如上,首先收集所有模組根saga,然後遍歷陣列,啟動每一個saga流根saga。

saga例項

以AppSaga為例,我們期望在應用啟動時就發起一些非同步請求,如獲取文章列表資料將其填充至redux store,而不等待使用資料的元件渲染完才開始請求資料,提高響應速度:

const REQUEST_POST_LIST = 'REQUEST_POST_LIST'
const RECEIVE_POST_LIST = 'RECEIVE_POST_LIST'
/**
* 請求文章列表ActionCreator
* @param {object} payload
*/
function requestPostList (payload) {
return {
type: REQUEST_POST_LIST,
payload: payload
}
}
/**
* 接收文章列表ActionCreator
* @param {*} payload
*/
function receivePostList (payload) {
return {
type: RECEIVE_POST_LIST,
payload: payload
}
}
/**
* 處理請求文章列表Saga
* @param {*} payload 請求引數負載
*/
function * getPostListSaga ({ payload }) {
const data = yield call(getPostList)
yield put(receivePostList(data))
}
// 定義AppSaga
export function * AppSaga (action) {
// 接收最近一次請求,然後呼叫getPostListSaga子Saga
yield takeLatest(REQUEST_POST_LIST, getPostListSaga)
}

takeLatest

put

總結

本文較詳細的總結了個人從0到1搭建一個專案架構的過程,對React, Redux應用和專案工程實踐都有了更深的理解及思考,在大前端成長之路繼續砥礪前行。

注:文中列出的所有技術棧,博主計劃一步一步推進,目前原始碼中使用的技術有React,React Router,Redux,react-redux,react-router-redux,Redux-saga,axios。後期計劃推進Immutable,Reactotron,Redux Persist。

完整專案程式碼見github

參考

React
Redux
React Router v4
redux-saga
Redux Persist

相關文章

前端開發 最新文章