React應用架構設計指南

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

codingplayboy/react-bloggithub.com圖標React應用架構設計 - 熊建剛的博客blog.codingplayboy.com圖標

前言

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

項目結構與技術棧

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

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

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

生成項目結構如下圖:

  1. src為應用源代碼目錄;
  2. webpack為webpack配置目錄;
  3. webpack.config.js為webpack配置入口文件;
  4. package.json為項目依賴管理文件;
  5. yarn.lock為項目依賴版本鎖文件;
  6. .babelrc文件,babel的配置文件,使用babel編譯React和JavaScript代碼;
  7. eslintrceslintignore分別為eslint語法檢測配置及需要忽略檢查的內容或文件;
  8. postcss.config.js為CSS後編譯器postcss的配置文件;
  9. API.md為API文檔入口;
  10. docs為文檔目錄;
  11. README.md為項目說明文檔;

接下來的工作主要就是豐富src目錄,包括搭建項目架構,開發應用功能,還有自動化,單元測試等,本篇主要關注項目架構的搭建,然後使用技術棧實踐開發幾個模塊。

技術棧

項目架構搭建很大部分依賴於項目的技術棧,所以先對整個技術棧進行分析,總結:

  1. react和react-dom庫是項目前提;
  2. react路由;
  3. 應用狀態管理容器;
  4. 是否需要Immutable數據;
  5. 應用狀態的持久化;
  6. 非同步任務管理;
  7. 測試及輔助工具或函數;
  8. 開發調試工具;

根據以上劃分決定選用以下第三方庫和工具構成項目的完整技術棧:

  1. react,react-dom;
  2. react-router管理應用路由;
  3. redux作為JavaScript狀態容器,react-redux將React應用與redux連接;
  4. Immutable.js支持Immutable化狀態,redux-immutable使整個redux store狀態樹Immutable化;
  5. 使用redux-persist支持redux狀態樹的持久化,並添加redux-persist-immutable拓展以支持Immutable化狀態樹的持久化;
  6. 使用redux-saga管理應用內的非同步任務,如網路請求,非同步讀取本地數據等;
  7. 使用jest集成應用測試,使用lodash,ramda等可選輔助類,工具類庫;
  8. 可選使用reactotron調試工具

針對以上分析,完善後的項目結構如圖:

directory

開發調試工具

React應用開發目前已經有諸多調試工具,常用的如redux-devtools,Reactron等。

redux-devtools

redux-devtools是支持熱重載,回放action,自定義UI的一款Redux開發工具。

首先需要按照對應的瀏覽器插件,然後再Redux應用中添加相關配置,就能在瀏覽器控制台中查看到redux工具欄了,詳細文檔點此查看。

然後安裝項目依賴庫:

yarn add --dev redux-devtools

然後在創建redux store時將其作為redux強化器傳入createStore方法:

import { applyMiddleware, compose, createStore, combineReducers } from redux// 默認為redux提供的組合函數let composeEnhancers = composeif (__DEV__) { // 開發環境,開啟redux-devtools const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ if (typeof composeWithDevToolsExtension === function) { // 支持redux開發工具拓展的組合函數 composeEnhancers = composeWithDevToolsExtension }}// create storeconst store = createStore( combineReducers(...), initialState, // 組合redux中間價和加強器,強化redux composeEnhancers( applyMiddleware(...middleware), ...enhancers ))

  1. 在開發環境下獲取redux-devtools提供的拓展組合函數;
  2. 創建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(); // Lets 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方法拓展入口組件:

import ./config/ReactotronConfig;import DebugConfig from ./config/DebugConfig;class App extends Component { render () { return ( <Provider store={store}> <AppContainer /> </Provider> ) }}// allow reactotron overlay for fast design in dev modeexport default DebugConfig.useReactotron ? console.tron.overlay(App) : App

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

組件劃分

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

  1. 布局組件:僅僅涉及應用UI界面結構的組件,不涉及任何業務邏輯,數據請求及操作;
  2. 容器組件:負責獲取數據,處理業務邏輯,通常在render()函數內返回展示型組件;
  3. 展示型組件:負責應用的界面UI展示;
  4. UI組件:指抽象出的可重用的UI獨立組件,通常是無狀態組件;

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

Redux

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

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

  1. 數據單一來源原則:使用Redux作為應用狀態管理容器,統一管理應用的狀態樹,它推從數據單一可信來源原則,所有數據都來自redux store,所有的數據更新也都由redux處理;
  2. redux store狀態樹:redux集中管理應用狀態,組織管理形式就好比DOM樹和React組件樹一樣,以樹的形式組織,簡單高效;
  3. redux和store:redux是一種Flux的實現方案,所以創建了store一詞,它類似於商店,集中管理應用狀態,支持將每一個發布的action分發至所有reducer;
  4. action:以對象數據格式存在,通常至少有type和payload屬性,它是對redux中定義的任務的描述;
  5. reducer:通常是以函數形式存在,接收state(應用局部狀態)和action對象兩個參數,根據action.type(action類型)執行不同的任務,遵循函數式編程思想;
  6. dispatch:store提供的分發action的功能方法,傳遞一個action對象參數;
  7. createStore:創建store的方法,接收reducer,初始應用狀態,redux中間件和增強器,初始化store,開始監聽action;

中間件(Redux Middleware)

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

增強器(Store Enhancer)

有些時候我們可能並不滿足於拓展dispatch方法,還希望能增強store,redux提供以增強器形式增強store的各個方面,甚至可以完全定製一個store對象上的所有介面,而不僅僅是store.dispatch方法。

const logEnhancer = (createStore) => (reducer, preloadedState, enhancer) => { const store = createStore(reducer, preloadedState, enhancer) const originalDispatch = store.dispatch store.dispatch = (action) => { console.log(action) originalDispatch(action) } return store}

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

可以看到,增強器完全能實現中間件的功能,其實,中間件就是以增強器方式實現的,它提供的compose方法就可以組合將我們傳入的增強器拓展到store,而如果我們傳入中間件,則需要先調用applyMiddleware方法包裝,內部以增強器形式將中間件功能拓展到store.dispatch方法

react-redux

Redux是一個獨立的JavaScript應用狀態管理容器庫,它可以與React、Angular、Ember、jQuery甚至原生JavaScript應用配合使用,所以開發React應用時,需要將Redux和React應用連接起來,才能統一使用Redux管理應用狀態,使用官方提供的react-redux庫。

class App extends Component { render () { const { store } = this.props return ( <Provider store={store}> <div> <Routes /> </div> </Provider> ) }}

react-redux庫提供Provider組件通過context方式嚮應用注入store,然後可以使用connect高階方法,獲取並監聽store,然後根據store state和組件自身props計算得到新props,注入該組件,並且可以通過監聽store,比較計算出的新props判斷是否需要更新組件。

更多關於react-redux的內容可以閱讀之前的文章:React-Redux分析。

createStore

使用redux提供的createStore方法創建redux store,但是在實際項目中我們常常需要拓展redux添加某些自定義功能或服務,如添加redux中間件,添加非同步任務管理saga,增強redux等:

// creates the storeexport default (rootReducer, rootSaga, initialState) => { /* ------------- Redux Configuration ------------- */ // Middlewares // Build the middleware for intercepting and dispatching navigation actions const blogRouteMiddleware = routerMiddleware(history) const sagaMiddleware = createSagaMiddleware() const middleware = [blogRouteMiddleware, sagaMiddleware] // enhancers const enhancers = [] let composeEnhancers = compose // create store const store = createStore( combineReducers({ router: routerReducer, ...reducers }), initialState, composeEnhancers( applyMiddleware(...middleware), ...enhancers ) ) sagaMiddleware.run(saga) return store;}

redux與Immutable

redux默認提供了combineReducers方法整合reduers至redux,然而該默認方法期望接受原生JavaScript對象並且它把state作為原生對象處理,所以當我們使用createStore方法並且接受一個Immutable對象作應用初始狀態時,reducer將會返回一個錯誤,源代碼如下:

if (!isPlainObject(inputState)) { return ( `The ${argumentName} has unexpected type of "` + ({}).toString.call(inputState).match(/s([a-z|A-Z]+)/)[1] + ".Expected argument to be an object with the following + `keys:"${reducerKeys.join(", ")}"` ) }

如上表明,原始類型reducer接受的state參數應該是一個原生JavaScript對象,我們需要對combineReducers其進行增強,以使其能處理Immutable對象,redux-immutable 即提供創建一個可以和Immutable.js協作的Redux combineReducers。

import { combineReducers } from redux-immutable;import Immutable from immutable;import configureStore from ./CreateStore;// use Immutable.Map to create the store state treeconst initialState = Immutable.Map();export default () => { // Assemble The Reducers const rootReducer = combineReducers({ ...RouterReducer, ...AppReducer }); return configureStore(rootReducer, rootSaga, initialState);}

如上代碼,可以看見我們傳入的initialState是一個Immutable.Map類型數據,我們將redux整個state樹叢根源開始Immutable化,另外傳入了可以處理Immutable state的reducers和sagas。

另外每一個state樹節點數據都是Immutable結構,如AppReducer

const initialState = Immutable.fromJS({ ids: [], posts: { list: [], total: 0, totalPages: 0 }})const AppReducer = (state = initialState, action) => { case RECEIVE_POST_LIST: const newState = state.merge(action.payload) return newState || state default: return state}

這裡默認使用Immutable.fromJS()方法狀態樹節點對象轉化為Immutable結構,並且更新state時使用Immutable方法state.merge(),保證狀態統一可預測。

React路由

在React web單頁面應用中,頁面級UI組件的展示和切換完全由路由控制,每一個路由都有對應的URL及路由信息,我們可以通過路由統一高效的管理我們的組件切換,保持UI與URL同步,保證應用的穩定性及友好體驗。

react-router

React Router是完整的React 路由解決方案,也是開發React應用最常使用的路由管理庫,只要用過它,絕對會喜歡上它的設計,它提供簡單的API,以聲明式方式實現強大的路由功能,諸如按需載入,動態路由等。

  1. 聲明式:語法簡潔,清晰;
  2. 按需載入:延遲載入,根據使用需要判斷是否需要載入;
  3. 動態路由:動態組合應用路由結構,更靈活,更符合組件化開發模式;

動態路由與靜態路由

使用react-router v4版本可以定義跨平台的應用動態路由結構,所謂的動態路由(Dynamic Routing)即在渲染過程中發生路由的切換,而不需要在創建應用前就配置好,這也正是其區別於靜態路由(Static Routing)所在,動態路由提高更靈活的路由組織方式,而且更方便編碼實現路由按需載入組件。

在react-router v2和v3版本中,開發React應用需要在開始渲染前就定義好完整的應用路由結構,所有的路由都需要同時初始化,才能在應用渲染後生效,會產生很多嵌套化路由,喪失了動態路由的靈活性和簡潔的按需載入編碼方式。

react-router v4.x

在react-router 2.x和3.x版本中,定義一個應用路由結構通常如下:

import React from reactimport ReactDOM from react-domimport { browserHistory, Router, Route, IndexRoute } from react-routerimport App from ../components/Appimport Home from ../components/Homeimport About from ../components/Aboutimport Features from ../components/FeaturesReactDOM.render( <Router history={browserHistory}> <Route path=/ component={App}> <IndexRoute component={Home} /> <Route path=about component={About} /> <Route path=features component={Features} /> </Route> </Router>, document.getElementById(app))

很簡單,但是所有的路由結構都需要在渲染應用前,統一定義,層層嵌套;而且如果要實現非同步按需載入還需要在這裡對路由配置對象進行修改,使用getComponentAPI,並侵入改造該組件,配合webpack的非同步打包載入API,實現按需載入:

  1. 路由層層嵌套,必須在渲染應用前統一聲明;
  2. API不同,需要使用getComponent,增加路由配置對象的複雜性;
  3. <Route>只是一個聲明路由的輔助標籤,本身無意義;

而使用react-router v4.x則如下:

// react-dom (what well use here)import { BrowserRouter } from react-router-domReactDOM.render(( <BrowserRouter> <App/> </BrowserRouter>), el)const App = () => ( <div> <nav> <Link to="/about">Dashboard</Link> </nav> <Home /> <div> <Route path="/about" component={About}/> <Route path="/features" component={Features}/> </div> </div>)

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

  1. 靈活性:路由可以在渲染組件中聲明,不需依賴於其他路由,不需要集中配置;
  2. 簡潔:統一傳入component,保證路由聲明的簡潔性;
  3. 組件化:<Route>作為一個真實組件創建路由,可以渲染;

路由鉤子方法

另外需要注意的是,相對於之前版本提供onEnter, onUpdate, onLeave等鉤子方法API在一定程度上提高了對路由的可控性,但是實質只是覆蓋了渲染組件的生命周期方法,現在我們可以通過路由渲染組件的生命周期方法直接控制路由,如使用componentDidMountcomponentWillMount 代替 onEnter

路由與Redux

同時使用React-Router和Redux時,大多數情況是正常的,但是也可能出現路由變更組件未更新的情況,如:

  1. 我們使用redux的connect方法將組件連接至redux:connect(Home);
  2. 組件不是一個路由渲染組件,即不是使用Route>組件形式:<Route component={Home} />聲明渲染的;

這是為什麼呢?,因為Redux會實現組件的shouldComponentUpdate方法,當路由變化時,該組件並沒有接收到props表明發生了變更,需要更新組件。

那麼如何解決問題呢?,要解決這個問題只需要簡單的使用react-router-dom提供的withRouter方法包裹組件:

import { withRouter } from react-router-domexport default withRouter(connect(mapStateToProps)(Home))

Redux整合

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

REACT-ROUTER-REDUX

連接React Router與Redux,需要使用react-router-redux庫,而且react-router v4版本需要指定安裝@next版本和hsitory庫:

yarn add react-router-redux@nextyarn add history

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

  1. 創建一個history對象,對於web應用,我們選擇browserHisotry,對應需要從history/createBrowserHistory模塊引入createHistory方法以創建history對象;

點此查看更多history相關內容

  1. 添加routerReducerrouterMiddleware中間件「,其中routerMiddleware中間件接收history對象參數,連接store和history,等同於舊版本的syncHistoryWithStore

import createHistory from history/createBrowserHistoryimport { ConnectedRouter, routerReducer, routerMiddleware, push } from react-router-redux// Create a history of your choosing (were using a browser history in this case)export const history = createHistory()// Build the middleware for intercepting and dispatching navigation actionsconst middleware = routerMiddleware(history)// Add the reducer to your store on the `router` key// Also apply our middleware for navigatingconst store = createStore( combineReducers({ ...reducers, router: routerReducer }), applyMiddleware(middleware))return store

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

  1. 初始化渲染根組件,掛載至DOM的根組件,由<Provider>組件包裹,注入store;
  2. 路由配置組件,在根組件中,聲明路由配置組件,初始化必要的應用路由定義及路由對象;

import createStore from ./store/import Routes from ./routes/import appReducer from ./store/appReduxconst store = createStore({}, { app: appReducer})/** * 項目根組件 * @class App * @extends Component */class App extends Component { render () { const { store } = this.props return ( <Provider store={store}> <div> <Routes /> </div> </Provider> ) }}// 渲染根組件ReactDOM.render( <App store={store} />, document.getElementById(app))

上面的<Routes>組件是項目的路由組件:

import { history } from ../store/import { ConnectedRouter } from react-router-reduximport { Route } from react-routerclass Routes extends Component { render () { return ( <ConnectedRouter history={history}> <div> <BlogHeader /> <div> <Route exact path=/ component={Home} /> <Route exact path=/posts/:id component={Article} /> </div> </div> </ConnectedRouter> ) }}

首先使用react-router-redux提供的ConnectedRouter組件包裹路由配置,該組件將自動使用<Provider>組件注入的store,我們需要做的是手動傳入history屬性,在組件內會調用history.listen方法監聽瀏覽器LOCATION_CHANGE事件,最後返回react-router<Router >組件,處理作為this.props.children傳入的路由配置,ConnectedRouter組件內容傳送。

DISPATCH切換路由

配置上面代碼後,就能夠以dispatch action的方式觸發路由切換和組件更新了:

import { push } from react-router-redux// Now you can dispatch navigation actions from anywhere!store.dispatch(push(/about))

這個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,它提供persistStoreautoRehydrate方法分別持久化本地存儲store及恢復啟動store,另外還支持自定義傳入持久化及恢復store時對store state的轉換拓展。

yarn add redux-persist

持久化STORE

如下在創建store時會調用persistStore相關服務-RehydrationServices.updateReducers()

// configure persistStore and check reducer version numberif (ReduxPersistConfig.active) { RehydrationServices.updateReducers(store);}

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

// Check to ensure latest reducer versionstorage.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即可。

persistStore(store, [config], [callback])

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

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

接收參數主要如下:

  1. store: 持久化的store;
  2. config:配置對象
    1. storage:一個 持久化引擎,例如 LocalStorage 和 AsyncStorage;
    2. transforms: 在 rehydration 和 storage 階段被調用的轉換器;
    3. blacklist: 黑名單數組,指定持久化忽略的 reducers 的 key;
  1. callback:ehydration 操作結束後的回調;

恢復啟動

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

// add the autoRehydrate enhancerif (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方法替換redux-persist所提供的方法:

import { persistStore } from redux-persist-immutable;

TRANSFORM

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

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

如上,輸出對象中的in和out分別對應持久化及恢複數據時的轉換操作,實現的只是使用fromJS()toJS()轉換Js和Immutable數據結構,使用方式如下:

import immutablePersistenceTransform from ../services/ImmutablePersistenceTransformpersistStore(store, { transforms: [immutablePersistenceTransform]}, startApp);

Immutable

在項目中引入Immutable以後,需要盡量保證以下幾點:

  1. redux store整個state樹的統一Immutable化;
  2. redux持久化對Immutable數據的兼容;
  3. 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]),對象拓展符(…)等,存在一些問題:

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

針對這些問題,社區有了seamless-immutable可供替換選擇:

  1. 更輕:相對於Immutable.jsseamless-immutable庫更輕小;
  2. 語法:對象和數組的操作語法更貼近原生JavaScript;
  3. 和其他JavaScript庫協作更方便;

非同步任務流管理

最後要介紹的模塊是非同步任務管理,在應用開發過程中,最主要的非同步任務就是數據HTTP請求,所以我們講非同步任務管理,主要關注在數據HTTP請求的流程管理。

axios

本項目中使用axios作為HTTP請求庫,axios是一個Promise格式的HTTP客戶端,選擇此庫的原因主要有以下幾點:

  1. 能在瀏覽器發起XMLHttpRequest,也能在node.js端發起HTTP請求;
  2. 支持Promise;
  3. 能攔截請求和響應;
  4. 能取消請求;
  5. 自動轉換JSON數據;

redux-saga

redux-saga是一個致力於使應用中如數據獲取,本地緩存訪問等非同步任務易於管理,高效運行,便於測試,能更好的處理異常的三方庫。

Redux-saga是一個redux中間件,它就像應用中一個單獨的進程,只負責管理非同步任務,它可以接受應用主進程的redux action以決定啟動,暫停或者是取消進程任務,它也可以訪問redux應用store state,然後分發action。

初始化SAGA

redux-saga是一個中間件,所以首先調用createSagaMiddleware方法創建中間件,然後使用redux的applyMiddleware方法啟用中間件,之後使用compose輔助方法傳給createStore創建store,最後調用run方法啟動根saga:

import { createStore, applyMiddleware, compose } from redux;import createSagaMiddleware from redux-saga;import rootSaga from ../sagas/const sagaMiddleware = createSagaMiddleware({ sagaMonitor });middleware.push(sagaMiddleware);enhancers.push(applyMiddleware(...middleware));const store = createStore(rootReducer, initialState, compose(...enhancers));// kick off root sagasagaMiddleware.run(rootSaga);

SAGA分流

在項目中通常會有很多並列模塊,每個模塊的saga流也應該是並列的,需要以多分支形式並列,redux-saga提供的fork方法就是以新開分支的形式啟動當前saga流:

import { fork, takeEvery } from redux-saga/effectsimport { HomeSaga } from ./Home/flux.jsimport { AppSaga } from ./Appflux.jsconst sagas = [ ...AppSaga, ...HomeSaga]export default function * root() { yield sagas.map(saga => fork(saga))}

如上,首先收集所有模塊根saga,然後遍曆數組,啟動每一個saga流根saga。

SAGA實例

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

const REQUEST_POST_LIST = REQUEST_POST_LISTconst 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))}// 定義AppSagaexport function * AppSaga (action) { // 接收最近一次請求,然後調用getPostListSaga子Saga yield takeLatest(REQUEST_POST_LIST, getPostListSaga)}

  1. takeLatest:在AppSaga內使用takeLatest方法監聽REQUEST_POST_LISTaction,若短時間內連續發起多次action,則會取消前面未響應的action,只發起最後一次action;
  2. getPostListSaga子Saga:當接收到該action時,調用getPostListSaga,並將payload傳遞給它,getPostListSaga是AppSaga的子級Saga,在裡面處理具體非同步任務;
  3. getPostListgetPostListSaga會調用getPostList方法,發起非同步請求,拿到響應數據後,調用receivePostList ActionCreator,創建並分發action,然後由reducer處理相應邏輯;

getPostList方法內容如下:

/** * 請求文章列表方法 * @param {*} payload 請求參數 * eg: { * page: Num, * per_page: Num * } */function getPostList (payload) { return fetch({ ...API.getPostList, data: payload }).then(res => { if (res) { let data = formatPostListData(res.data) return { total: parseInt(res.headers[X-WP-Total.toLowerCase()], 10), totalPages: parseInt(res.headers[X-WP-TotalPages.toLowerCase()], 10), ...data } } })}

put是redux-saga提供的可分發action方法,take,call等都是redux-saga提供的API,更多內容查看API文檔。

之後便可以在項目路由根組件注入ActionCreator,創建action,然後saga就會接收進行處理了。

saga與Reactotron

前面已經配置好可以使用Reactotron捕獲應用所有redux和action,而redux-saga是一類redux中間件,所以捕獲sagas需要額外配置,創建store時,在saga中間件內添加sagaMonitor服務,監聽saga:

const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null;const sagaMiddleware = createSagaMiddleware({ sagaMonitor });middleware.push(sagaMiddleware);...

總結

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

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

codingplayboy/react-bloggithub.com圖標

參考

  1. React
  2. Redux
  3. React Router v4
  4. redux-saga
  5. Redux Persist

推薦閱讀:

【譯】Redux + React 應用程序架構的 3 條規範(內附實例)
React系列:React架構
沒有安卓和ios開發經驗的前端適合學rn嗎?
在React中使用RxJS
用 Three.js, React 和 WebGL 開發遊戲 — SitePoint

TAG:React | reactrouter | reactrouterredux |