React應(yīng)用架構(gòu)設(shè)計(react 架構(gòu)設(shè)計)
本節(jié)將開始詳細分析如何搭建一個React應(yīng)用架構(gòu)。
一. 前言
現(xiàn)在已經(jīng)有很多腳手架工具,如create-react-app,支持一鍵創(chuàng)建一個React應(yīng)用項目結(jié)構(gòu),很方便,但是享受方便的同時,也失去了對項目架構(gòu)及技術(shù)棧完整學(xué)習(xí)的機會,而且通常腳手架創(chuàng)建的應(yīng)用技術(shù)架構(gòu)并不能完全滿足我們的業(yè)務(wù)需求,需要我們自己修改,完善,所以如果希望對項目架構(gòu)有更深掌控,最好還是從0到1理解一個項目。
二. 項目結(jié)構(gòu)與技術(shù)棧
我們這次的實踐不準備使用任何腳手架,所以我們需要自己創(chuàng)建每一個文件,引入每一個技術(shù)和三方庫,最終形成完整的應(yīng)用,包括我們選擇的完整技術(shù)棧。
第一步,當(dāng)然是創(chuàng)建目錄,如果你還沒有代碼,可以從Github獲?。?/p>
git clone https://github.com/codingplayboy/react-blog.git cd react-blog
生成項目結(jié)構(gòu)如下圖:
1、 src為應(yīng)用源代碼目錄;
2、 webpack為webpack配置目錄;
3、 webpack.config.js為webpack配置入口文件;
4、 package.json為項目依賴管理文件;
5、 yarn.lock為項目依賴版本鎖文件;
6、 .babelrc文件,babel的配置文件,使用babel編譯React和JavaScript代碼;
7、 eslintrc 和 eslintignore 分別為eslint語法檢測配置及需要忽略檢查的內(nèi)容或文件;
8、 postcss.config.js 為CSS后編譯器postcss的配置文件;
9、 API.md為API文檔入口;
10、 docs為文檔目錄;
11、 README.md為項目說明文檔;
接下來的工作主要就是豐富src目錄,包括搭建項目架構(gòu),開發(fā)應(yīng)用功能,還有自動化,單元測試等,本篇主要關(guān)注項目架構(gòu)的搭建,然后使用技術(shù)棧實踐開發(fā)幾個模塊。
2.1 技術(shù)棧
項目架構(gòu)搭建很大部分依賴于項目的技術(shù)棧,所以先對整個技術(shù)棧進行分析,總結(jié):
1、 react和react-dom庫是項目前提;
2、 react路由;
3、 應(yīng)用狀態(tài)管理容器;
4、 是否需要Immutable數(shù)據(jù);
5、 應(yīng)用狀態(tài)的持久化;
6、 異步任務(wù)管理;
7、 測試及輔助工具或函數(shù);
8、 開發(fā)調(diào)試工具;
根據(jù)以上劃分決定選用以下第三方庫和工具構(gòu)成項目的完整技術(shù)棧:
1、react,react-dom;
2、react-router管理應(yīng)用路由;
3、Redux作為JavaScript狀態(tài)容器,react-redux將React應(yīng)用與redux連接;
4、Immutable.js支持Immutable化狀態(tài),redux-immutable使整個redux store狀態(tài)樹Immutable化;
5、使用redux-persist支持redux狀態(tài)樹的持久化,并添加redux-persist-immutable拓展以支持Immutable化狀態(tài)樹的持久化;
6、使用redux-saga管理應(yīng)用內(nèi)的異步任務(wù),如網(wǎng)絡(luò)請求,異步讀取本地數(shù)據(jù)等;
7、使用jest集成應(yīng)用測試,使用lodash,ramda等可選輔助類,工具類庫;
8、可選使用reactotron調(diào)試工具
針對以上分析,完善后的項目結(jié)構(gòu)如圖:
三. 開發(fā)調(diào)試工具
React應(yīng)用開發(fā)目前已經(jīng)有諸多調(diào)試工具,常用的如redux-devtools,Reactron等。
3.1 redux-devtool
redux-devtools是支持熱重載,回放action,自定義UI的一款Redux開發(fā)工具。
首先需要按照對應(yīng)的瀏覽器插件,然后再Redux應(yīng)用中添加相關(guān)配置,就能在瀏覽器控制臺中查看到redux工具欄了。
然后安裝項目依賴庫:
yarn add –dev redux-devtools
然后在創(chuàng)建redux store時將其作為redux強化器傳入createStore方法:
import { applymiddleware, compose, createStore, combineReducers } from ‘redux’ // 默認為redux提供的組合函數(shù)let composeEnhancers = composeif (__DEV__) { // 開發(fā)環(huán)境,開啟redux-devtools const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ if (typeof composeWithDevToolsExtension === ‘function’) { // 支持redux開發(fā)工具拓展的組合函數(shù) composeEnhancers = composeWithDevToolsExtension }}// create storeconst store = createStore( combineReducers(…), initialState, // 組合redux中間價和加強器,強化redux composeEnhancers( applyMiddleware(…middleware), …enhancers ))
在開發(fā)環(huán)境下獲取redux-devtools提供的拓展組合函數(shù);
創(chuàng)建store時使用拓展組合函數(shù)組合redux中間件和增強器,redux-dev-tools便獲得了應(yīng)用redux的相關(guān)信息;
3.2 Reactotron
Reactotron是一款跨平臺調(diào)試React及React Native應(yīng)用的桌面應(yīng)用,能動態(tài)實時監(jiān)測并輸出React應(yīng)用等redux,action,saga異步請求等信息,如圖:
首先安裝:
yarn add –dev reactotron-react-js
然后初始化Reactotron相關(guān)配置:
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方法拓展入口組件:
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客戶端捕獲應(yīng)用中發(fā)起的所有的redux和action了。
四. 組件劃分
React組件化開發(fā)原則是組件負責(zé)渲染UI,組件不同狀態(tài)對應(yīng)不同UI,通常遵循以下組件設(shè)計思路:
1、 布局組件:僅僅涉及應(yīng)用UI界面結(jié)構(gòu)的組件,不涉及任何業(yè)務(wù)邏輯,數(shù)據(jù)請求及操作;
2、 容器組件:負責(zé)獲取數(shù)據(jù),處理業(yè)務(wù)邏輯,通常在render()函數(shù)內(nèi)返回展示型組件;
3、 展示型組件:負責(zé)應(yīng)用的界面UI展示;
4、 UI組件:指抽象出的可重用的UI獨立組件,通常是無狀態(tài)組件;
五. Redux
現(xiàn)在的任何大型web應(yīng)用如果少了狀態(tài)管理容器,那這個應(yīng)用就缺少了時代特征,可選的庫諸如mobx,redux等,實際上大同小異,各取所需,以redux為例,redux是最常用的React應(yīng)用狀態(tài)容器庫,對于React Native應(yīng)用也適用。
Redux是一個JavaScript應(yīng)用的可預(yù)測狀態(tài)管理容器,它不依賴于具體框架或類庫,所以它在多平臺的應(yīng)用開發(fā)中有著一致的開發(fā)方式和效率,另外它還能幫我們輕松的實現(xiàn)時間旅行,即action的回放。
1、 數(shù)據(jù)單一來源原則:使用Redux作為應(yīng)用狀態(tài)管理容器,統(tǒng)一管理應(yīng)用的狀態(tài)樹,它推從數(shù)據(jù)單一可信來源原則,所有數(shù)據(jù)都來自redux store,所有的數(shù)據(jù)更新也都由redux處理;
2、 redux store狀態(tài)樹:redux集中管理應(yīng)用狀態(tài),組織管理形式就好比DOM樹和React組件樹一樣,以樹的形式組織,簡單高效;
3、 redux和store:redux是一種Flux的實現(xiàn)方案,所以創(chuàng)建了store一詞,它類似于商店,集中管理應(yīng)用狀態(tài),支持將每一個發(fā)布的action分發(fā)至所有reducer;
4、 action:以對象數(shù)據(jù)格式存在,通常至少有type和payload屬性,它是對redux中定義的任務(wù)的描述;
5、 reducer:通常是以函數(shù)形式存在,接收state(應(yīng)用局部狀態(tài))和action對象兩個參數(shù),根據(jù)action.type(action類型)執(zhí)行不同的任務(wù),遵循函數(shù)式編程思想;
6、dispatch:store提供的分發(fā)action的功能方法,傳遞一個action對象參數(shù);
7、 createStore:創(chuàng)建store的方法,接收reducer,初始應(yīng)用狀態(tài),redux中間件和增強器,初始化store,開始監(jiān)聽action;
5.1 中間件(Redux Middleware)
Redux中間件,和Node中間件一樣,它可以在action分發(fā)至任務(wù)處理reducer之前做一些額外工作,dispatch發(fā)布的action將依次傳遞給所有中間件,最終到達reducer,所以我們使用中間件可以拓展諸如記錄日志,添加監(jiān)控,切換路由等功能,所以中間件本質(zhì)上只是拓展了store.dispatch方法。
5.2 增強器(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}
最簡單的例子代碼如上,新函數(shù)接收redux的createStore方法和創(chuàng)建store需要的參數(shù),然后在函數(shù)內(nèi)部保存store對象上某方法的引用,重新實現(xiàn)該方法,在里面處理完增強邏輯后調(diào)用原始方法,保證原始功能正常執(zhí)行,這樣就增強了store的dispatch方法。
可以看到,增強器完全能實現(xiàn)中間件的功能,其實,中間件就是以增強器方式實現(xiàn)的,它提供的compose方法就可以組合將我們傳入的增強器拓展到store,而如果我們傳入中間件,則需要先調(diào)用applyMiddleware方法包裝,內(nèi)部以增強器形式將中間件功能拓展到store.dispatch方法
5.3 react-redux
Redux是一個獨立的JavaScript應(yīng)用狀態(tài)管理容器庫,它可以與React、Angular、Ember、jQuery甚至原生JavaScript應(yīng)用配合使用,所以開發(fā)React應(yīng)用時,需要將Redux和React應(yīng)用連接起來,才能統(tǒng)一使用Redux管理應(yīng)用狀態(tài),使用官方提供的react-redux庫。
class App extends Component { render () { const { store } = this.props return ( <Provider store={store}> <div> <Routes /> </div> </Provider> ) }}
react-redux庫提供Provider組件通過context方式向應(yīng)用注入store,然后可以使用connect高階方法,獲取并監(jiān)聽store,然后根據(jù)store state和組件自身props計算得到新props,注入該組件,并且可以通過監(jiān)聽store,比較計算出的新props判斷是否需要更新組件。
5.4 createStore
使用redux提供的createStore方法創(chuàng)建redux store,但是在實際項目中我們常常需要拓展redux添加某些自定義功能或服務(wù),如添加redux中間件,添加異步任務(wù)管理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;}
5.5 redux與Immutable
redux默認提供了combineReducers方法整合reduers至redux,然而該默認方法期望接受原生JavaScript對象并且它把state作為原生對象處理,所以當(dāng)我們使用createStore方法并且接受一個Immutable對象作應(yīng)用初始狀態(tài)時,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參數(shù)應(yīng)該是一個原生JavaScript對象,我們需要對combineReducers其進行增強,以使其能處理Immutable對象,redux-immutable即提供創(chuàng)建一個可以和Immutable.js協(xié)作的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類型數(shù)據(jù),我們將redux整個state樹叢根源開始Immutable化,另外傳入了可以處理Immutable state的reducers和sagas。另外每一個state樹節(jié)點數(shù)據(jù)都是Immutable結(jié)構(gòu),如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()方法狀態(tài)樹節(jié)點對象轉(zhuǎn)化為Immutable結(jié)構(gòu),并且更新state時使用Immutable方法state.merge(),保證狀態(tài)統(tǒng)一可預(yù)測。
六. React路由
在React web單頁面應(yīng)用中,頁面級UI組件的展示和切換完全由路由控制,每一個路由都有對應(yīng)的URL及路由信息,我們可以通過路由統(tǒng)一高效的管理我們的組件切換,保持UI與URL同步,保證應(yīng)用的穩(wěn)定性及友好體驗。
6.1 react-router
React Router是完整的React 路由解決方案,也是開發(fā)React應(yīng)用最常使用的路由管理庫,只要用過它,絕對會喜歡上它的設(shè)計,它提供簡單的API,以聲明式方式實現(xiàn)強大的路由功能,諸如按需加載,動態(tài)路由等。
1、聲明式:語法簡潔,清晰;
2、按需加載:延遲加載,根據(jù)使用需要判斷是否需要加載;
3、動態(tài)路由:動態(tài)組合應(yīng)用路由結(jié)構(gòu),更靈活,更符合組件化開發(fā)模式;
6.2 動態(tài)路由與靜態(tài)路由
使用react-router v4版本可以定義跨平臺的應(yīng)用動態(tài)路由結(jié)構(gòu),所謂的動態(tài)路由(Dynamic Routing)即在渲染過程中發(fā)生路由的切換,而不需要在創(chuàng)建應(yīng)用前就配置好,這也正是其區(qū)別于靜態(tài)路由(Static Routing)所在,動態(tài)路由提高更靈活的路由組織方式,而且更方便編碼實現(xiàn)路由按需加載組件。
在react-router v2和v3版本中,開發(fā)React應(yīng)用需要在開始渲染前就定義好完整的應(yīng)用路由結(jié)構(gòu),所有的路由都需要同時初始化,才能在應(yīng)用渲染后生效,會產(chǎn)生很多嵌套化路由,喪失了動態(tài)路由的靈活性和簡潔的按需加載編碼方式。
6.3 react-router v4.x
在react-router 2.x和3.x版本中,定義一個應(yīng)用路由結(jié)構(gòu)通常如下:
import React from ‘react’ import ReactDOM from ‘react-dom’ import { browserHistory, Router, Route, IndexRoute } from ‘react-router’import App from ‘../components/App’ import Home from ‘../components/Home’ import About from ‘../components/About’ import Features from ‘../components/Features’ReactDOM.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’))
很簡單,但是所有的路由結(jié)構(gòu)都需要在渲染應(yīng)用前,統(tǒng)一定義,層層嵌套;而且如果要實現(xiàn)異步按需加載還需要在這里對路由配置對象進行修改,使用getComponentAPI,并侵入改造該組件,配合webpack的異步打包加載API,實現(xiàn)按需加載:
1、 路由層層嵌套,必須在渲染應(yīng)用前統(tǒng)一聲明;
2、 API不同,需要使用getComponent,增加路由配置對象的復(fù)雜性;
3、 <Route>只是一個聲明路由的輔助標簽,本身無意義;
而使用react-router v4.x則如下:
// react-dom (what we’ll use here)import { BrowserRouter } from ‘react-router-dom’ReactDOM.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>)
相比之前版本,減少了配置化的痕跡,更凸顯了組件化的組織方式,而且在渲染組件時才實現(xiàn)該部分路由,而如果期望按需加載該組件,則可以通過封裝實現(xiàn)一個支持異步加載組件的高階組件,將經(jīng)過高階組件處理后返回的組件傳入<Route>即可,依然遵循組件化形式:
1、 靈活性:路由可以在渲染組件中聲明,不需依賴于其他路由,不需要集中配置;
2、 簡潔:統(tǒng)一傳入component,保證路由聲明的簡潔性;
3、 組件化:<Route>作為一個真實組件創(chuàng)建路由,可以渲染;
6.3.1 路由鉤子方
另外需要注意的是,相對于之前版本提供 onEnter, onUpdate, onLeave 等鉤子方法API在一定程度上提高了對路由的可控性,但是實質(zhì)只是覆蓋了渲染組件的生命周期方法,現(xiàn)在我們可以通過路由渲染組件的生命周期方法直接控制路由,如使用 componentDidMount 或 componentWillMount 代替 onEnter。
6.4 路由與Redux
同時使用React-Router和Redux時,大多數(shù)情況是正常的,但是也可能出現(xiàn)路由變更組件未更新的情況,如:
1、 我們使用redux的 connect 方法將組件連接至redux:connect(Home);
2、 組件不是一個路由渲染組件,即不是使用 Route> 組件形式:<Route component={Home} /> 聲明渲染的;
這是為什么呢?,因為Redux會實現(xiàn)組件的 shouldComponentUpdate 方法,當(dāng)路由變化時,該組件并沒有接收到props表明發(fā)生了變更,需要更新組件。
那么如何解決問題呢?,要解決這個問題只需要簡單的使用react-router-dom 提供的 withRouter方法包裹組件:
import { withRouter } from ‘react-router-dom’ export default withRouter(connect(mapStateToProps)(Home))
6.5 Redux整合
在使用Redux以后,需要遵循redux的原則:單一可信數(shù)據(jù)來源,即所有數(shù)據(jù)來源都只能是reudx store,react路由狀態(tài)也不應(yīng)例外,所以需要將路由state與store state連接。
6.5.1 react-router-redux
連接React Router與Redux,需要使用 react-router-redux 庫,而且react-router v4版本需要指定安裝 @next 版本和 hsitory 庫:
yarn add react-router-redux@next yarn add history
然后,在創(chuàng)建store時,需要實現(xiàn)如下配置:
1、 創(chuàng)建一個history對象,對于web應(yīng)用,我們選擇browserHisotry,對應(yīng)需要從 history/createBrowserHistory 模塊引入 createHistory 方法以創(chuàng)建history對象;
2、 添加 routerReducer 和 routerMiddleware 中間件“,其中 routerMiddleware 中間件接收history對象參數(shù),連接store和history,等同于舊版本的 syncHistoryWithStore ;
import createHistory from ‘history/createBrowserHistory’ import { ConnectedRouter, routerReducer, routerMiddleware, push } from ‘react-router-redux’ // Create a history of your choosing (we’re 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、 路由配置組件,在根組件中,聲明路由配置組件,初始化必要的應(yīng)用路由定義及路由對象;
import createStore from ‘./store/’ import Routes from ‘./routes/’ import appReducer from ‘./store/appRedux’const 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-redux’ import { Route } from ‘react-router’class 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 屬性,在組件內(nèi)會調(diào)用 history.listen 方法監(jiān)聽瀏覽器 LOCATION_CHANGE 事件,最后返回 react-router 的 <Router > 組件,處理作為 this.props.children 傳入的路由配置。
6.5.2 dispatch切換路由
配置上面代碼后,就能夠以dispatch action的方式觸發(fā)路由切換和組件更新了:
import { push } from ‘react-router-redux’ // Now you can dispatch navigation actions from anywhere!store.dispatch(push(‘/about’))
這個reducer所做的只是將App導(dǎo)航路由狀態(tài)合并入store。
七. redux持久化
我們知道瀏覽器默認有資源的緩存功能并且提供本地持久化存儲方式如localStorage,indexDb,webSQL等,通??梢詫⒛承?shù)據(jù)存儲在本地,在一定周期內(nèi),當(dāng)用戶再次訪問時,直接從本地恢復(fù)數(shù)據(jù),可以極大提高應(yīng)用啟動速度,用戶體驗更有優(yōu)勢,我們可以使用localStorage存儲一些數(shù)據(jù),如果是較大量數(shù)據(jù)存儲可以使用webSQL。
另外不同于以往的直接存儲數(shù)據(jù),啟動應(yīng)用時本地讀取然后恢復(fù)數(shù)據(jù),對于redux應(yīng)用而言,如果只是存儲數(shù)據(jù),那么我們就得為每一個reducer拓展,當(dāng)再次啟動應(yīng)用時去讀取持久化的數(shù)據(jù),這是比較繁瑣而且低效的方式,是否可以嘗試存儲reducer key,然后根據(jù)key恢復(fù)對應(yīng)的持久化數(shù)據(jù),首先注冊Rehydrate reducer,當(dāng)觸發(fā)action時根據(jù)其reducer key恢復(fù)數(shù)據(jù),然后只需要在應(yīng)用啟動時分發(fā)action,這也很容易抽象成可配置的拓展服務(wù),實際上三方庫redux-persist已經(jīng)為我們做好了這一切。
7.1 redux-persist
要實現(xiàn)redux的持久化,包括redux store的本地持久化存儲及恢復(fù)啟動兩個過程,如果完全自己編寫實現(xiàn),代碼量比較復(fù)雜,可以使用開源庫 redux-persist,它提供 persistStore 和 autoRehydrate 方法分別持久化本地存儲store及恢復(fù)啟動store,另外還支持自定義傳入持久化及恢復(fù)store時對store state的轉(zhuǎn)換拓展。
yarn add redux-persist
7.1.1 持久化store
如下在創(chuàng)建store時會調(diào)用persistStore相關(guān)服務(wù)-
RehydrationServices.updateReducers():
// configure persistStore and check reducer version numberif (ReduxPersistConfig.active) { RehydrationServices.updateReducers(store);}
該方法內(nèi)實現(xiàn)了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版本號,這個是在應(yīng)用配置文件中可以配置,首次執(zhí)行持久化時存儲該版本號及store,若reducer版本號變更則清空原來存儲的store,否則傳入store給持久化方法 persistStore 即可。
persistStore(store, [config], [callback])
該方法主要實現(xiàn)store的持久化以及分發(fā)rehydration action :
1、 訂閱 redux store,當(dāng)其發(fā)生變化時觸發(fā)store存儲操作;
2、 從指定的StorageEngine(如localStorage)中獲取數(shù)據(jù),進行轉(zhuǎn)換,然后通過分發(fā) REHYDRATE action,觸發(fā) REHYDRATE 過程;
接收參數(shù)主要如下:
1、 store: 持久化的store;
2、 config:配置對象
1)storage:一個 持久化引擎,例如 LocalStorage 和 AsyncStorage;
2)transforms: 在 rehydration 和 storage 階段被調(diào)用的轉(zhuǎn)換器;
3) blacklist: 黑名單數(shù)組,指定持久化忽略的 reducers 的 key;
3、 callback:ehydration 操作結(jié)束后的回調(diào);
7.1.2 恢復(fù)啟動
和persisStore一樣,依然是在創(chuàng)建redux store時初始化注冊rehydrate拓展:
// add the autoRehydrate enhancerif (ReduxPersist.active) { enhancers.push(autoRehydrate());}
該方法實現(xiàn)的功能很簡單,即使用 持久化的數(shù)據(jù)恢復(fù)(rehydrate) store 中數(shù)據(jù),它其實是注冊了一個autoRehydarte reducer,會接收前文persistStore方法分發(fā)的rehydrate action,然后合并state。
當(dāng)然,autoRehydrate不是必須的,我們可以自定義恢復(fù)store方式:
import {REHYDRATE} from ‘redux-persist/constants’;//…case REHYDRATE: const incoming = action.payload.reducer if (incoming) { return { …state, …incoming } } return state;
7.1.3 版本更新
需要注意的是redux-persist庫已經(jīng)發(fā)布到v5.x,而本文介紹的以v5.x為例,v4.x參考此處,新版本有一些更新,可以選擇性決定使用哪個版本。
7.2 持久化與Immutable
前面已經(jīng)提到Redux與Immutable的整合,上文使用的redux -persist默認也只能處理原生JavaScript對象的redux store state,所以需要拓展以兼容Immutable。
7.2.1 redux-persist-immutable
使用redux-persist-immutable庫可以很容易實現(xiàn)兼容,所做的僅僅是使用其提供的 persistStore 方法替換redux-persist所提供的方法:
import { persistStore } from ‘redux-persist-immutable’;
7.2.2 transform
我們知道持久化store時,針對的最好是原生JavaScript對象,因為通常Immutable結(jié)構(gòu)數(shù)據(jù)有很多輔助信息,不易于存儲,所以需要定義持久化及恢復(fù)數(shù)據(jù)時的轉(zhuǎn)換操作:
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分別對應(yīng)持久化及恢復(fù)數(shù)據(jù)時的轉(zhuǎn)換操作,實現(xiàn)的只是使用 fromJS() 和 toJS() 轉(zhuǎn)換Js和Immutable數(shù)據(jù)結(jié)構(gòu),使用方式如下:
import immutablePersistenceTransform from ‘../services/ImmutablePersistenceTransform’ persistStore(store, { transforms: [immutablePersistenceTransform]}, startApp);
八. Immutable
在項目中引入Immutable以后,需要盡量保證以下幾點:
1、 redux store整個state樹的統(tǒng)一Immutable化;
2、 redux持久化對Immutable數(shù)據(jù)的兼容;
3、 React路由兼容Immutable;
8.1 Immutable與React路由
前面兩點已經(jīng)在前面兩節(jié)闡述過,第三點react-router兼容Immutable,其實就是使應(yīng)用路由狀態(tài)兼容Immutable,在React路由一節(jié)已經(jīng)介紹如何將React路由狀態(tài)連接至Redux store,但是如果應(yīng)用使用了Immutable庫,則還需要額外處理,將react-router state轉(zhuǎn)換為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;};
將默認初始路由狀態(tài)轉(zhuǎn)換為Immutable,并且路由變更時使用Immutable API操作state。
8.2 seamless-Immutable
當(dāng)引入Immutable.js后,對應(yīng)用狀態(tài)數(shù)據(jù)結(jié)構(gòu)的使用API就得遵循Immutable API,而不能再使用原生JavaScript對象,數(shù)組等的操作API了,諸如,數(shù)組解構(gòu)([a, b] = [b, c]),對象拓展符(…)等,存在一些問題:
1、Immutable數(shù)據(jù)輔助節(jié)點較多,數(shù)據(jù)較大:
2、必須使用Immutable語法,和JavaScript語法有差異,不能很好的兼容;
3、和Redux,react-router等JavaScript庫寫協(xié)作時,需要引入額外的兼容處理庫;
針對這些問題,社區(qū)有了 seamless-immutable 可供替換選擇:
1、更輕:相對于Immutable.js seamless-immutable 庫更輕?。?/p>
2、語法:對象和數(shù)組的操作語法更貼近原生JavaScript;
3、和其他JavaScript庫協(xié)作更方便;
九. 異步任務(wù)流管理
最后要介紹的模塊是異步任務(wù)管理,在應(yīng)用開發(fā)過程中,最主要的異步任務(wù)就是數(shù)據(jù)HTTP請求,所以我們講異步任務(wù)管理,主要關(guān)注在數(shù)據(jù)HTTP請求的流程管理。
9.1 axios
本項目中使用axios作為HTTP請求庫,axios是一個Promise格式的HTTP客戶端,選擇此庫的原因主要有以下幾點:
-
能在瀏覽器發(fā)起XMLHttpRequest,也能在node.js端發(fā)起HTTP請求;
支持Promise;
能攔截請求和響應(yīng);
能取消請求;
自動轉(zhuǎn)換JSON數(shù)據(jù);
9.2 redux-saga
redux-saga是一個致力于使應(yīng)用中如數(shù)據(jù)獲取,本地緩存訪問等異步任務(wù)易于管理,高效運行,便于測試,能更好的處理異常的三方庫。
Redux-saga是一個redux中間件,它就像應(yīng)用中一個單獨的進程,只負責(zé)管理異步任務(wù),它可以接受應(yīng)用主進程的redux action以決定啟動,暫?;蛘呤侨∠M程任務(wù),它也可以訪問redux應(yīng)用store state,然后分發(fā)action。
9.2.1 初始化saga
redux-saga是一個中間件,所以首先調(diào)用 createSagaMiddleware 方法創(chuàng)建中間件,然后使用redux的 applyMiddleware 方法啟用中間件,之后使用compose輔助方法傳給 createStore 創(chuàng)建store,最后調(diào)用 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);
9.2.2 saga分流
在項目中通常會有很多并列模塊,每個模塊的saga流也應(yīng)該是并列的,需要以多分支形式并列,redux-saga提供的 fork 方法就是以新開分支的形式啟動當(dāng)前saga流:
import { fork, takeEvery } from ‘redux-saga/effects’ import { HomeSaga } from ‘./Home/flux.js’ import { AppSaga } from ‘./Appflux.js’const sagas = [ …AppSaga, …HomeSaga]export default function * root() { yield sagas.map(saga => fork(saga))}
如上,首先收集所有模塊根saga,然后遍歷數(shù)組,啟動每一個saga流根saga。
9.2.3 saga實例
以AppSaga為例,我們期望在應(yīng)用啟動時就發(fā)起一些異步請求,如獲取文章列表數(shù)據(jù)將其填充至redux store,而不等待使用數(shù)據(jù)的組件渲染完才開始請求數(shù)據(jù),提高響應(yīng)速度:
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 請求參數(shù)負載*/function * getPostListSaga ({ payload }) { const data = yield call(getPostList) yield put(receivePostList(data))}// 定義AppSagaexport function * AppSaga (action) { // 接收最近一次請求,然后調(diào)用getPostListSaga子Saga yield takeLatest(REQUEST_POST_LIST, getPostListSaga)}
1. takeLatest:在AppSaga 內(nèi)使用 takeLatest 方法監(jiān)聽 REQUEST_POST_LIST action,若短時間內(nèi)連續(xù)發(fā)起多次action 則會取消前面未響應(yīng)的action,只發(fā)起最后一次action;
2. getPostListSaga子Saga:當(dāng)接收到該action時,調(diào)用getPostListSaga并將payload傳遞給它,getPostListSaga 是AppSaga的子級Saga,在里面處理具體異步任務(wù);
3. getPostList : getPostListSaga 會調(diào)用getPostList 方法,發(fā)起異步請求, 拿到響應(yīng)數(shù)據(jù)后,調(diào)用 receivePostList
ActionCreator,創(chuàng)建并分發(fā)action,然后由reducer處理相應(yīng)邏輯;
getPostList 方法內(nèi)容如下:
/*** 請求文章列表方法* @param {*} payload 請求參數(shù)* 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提供的可分發(fā)action方法,take,call等都是 redux-saga 提供的API。
之后便可以在項目路由根組件注入ActionCreator,創(chuàng)建action,然后saga就會接收進行處理了。
9.3 saga與Reactotron
前面已經(jīng)配置好可以使用Reactotron捕獲應(yīng)用所有redux和action,而redux-saga是一類redux中間件,所以捕獲sagas需要額外配置,創(chuàng)建store時,在saga中間件內(nèi)添加sagaMonitor服務(wù),監(jiān)聽saga:
const sagaMonitor = Config.useReactotron ? console.tron.createSagaMonitor() : null; const sagaMiddleware = createSagaMiddleware({ sagaMonitor }); middleware.push(sagaMiddleware); …
十. 總結(jié)
本文較詳細的總結(jié)了個人從0到1搭建一個項目架構(gòu)的過程,對React, Redux應(yīng)用和項目工程實踐都有了更深的理解及思考,在大前端成長之路繼續(xù)砥礪前行。
注:文中列出的所有技術(shù)棧,博主計劃一步一步推進,目前源碼中使用的技術(shù)有React,React Router,Redux,react-redux,react-router-redux,Redux-saga,axios。后期計劃推進Immutable,Reactotron,Redux Persist。
完整項目代碼見github
參考
1、 React
2、 Redux
3、 React Router v4
4、 redux-saga
5、 Redux Persist