在上一篇咱們介紹了Webpack自動化構建React應用,咱們的本地開發服務器能夠較好的支持咱們編寫React應用,而且支持代碼熱更新。本節將開始詳細分析如何搭建一個React應用架構。javascript
我的博客html
如今已經有不少腳手架工具,如create-react-app,支持一鍵建立一個React應用項目結構,很方便,可是享受方便的同時,也失去了對項目架構及技術棧完整學習的機會,並且一般腳手架建立的應用技術架構並不能徹底知足咱們的業務需求,須要咱們本身修改,完善,因此若是但願對項目架構有更深掌控,最好仍是從0到1理解一個項目。前端
咱們此次的實踐不許備使用任何腳手架,因此咱們須要本身建立每個文件,引入每個技術和三方庫,最終造成完整的應用,包括咱們選擇的完整技術棧。java
第一步,固然是建立目錄,咱們在上一篇已經弄好,若是你尚未代碼,能夠從Github獲取:node
git clone https://github.com/codingplayboy/react-blog.git
cd react-blog
複製代碼
生成項目結構以下圖:react
src
爲應用源代碼目錄;webpack
爲webpack配置目錄;webpack.config.js
爲webpack配置入口文件;package.json
爲項目依賴管理文件;yarn.lock
爲項目依賴版本鎖文件;.babelrc
文件,babel的配置文件,使用babel編譯React和JavaScript代碼;eslintrc
和eslintignore
分別爲eslint語法檢測配置及須要忽略檢查的內容或文件;postcss.config.js
爲CSS後編譯器postcss的配置文件;API.md
爲API文檔入口;docs
爲文檔目錄;README.md
爲項目說明文檔;接下來的工做主要就是豐富src
目錄,包括搭建項目架構,開發應用功能,還有自動化,單元測試等,本篇主要關注項目架構的搭建,而後使用技術棧實踐開發幾個模塊。webpack
項目架構搭建很大部分依賴於項目的技術棧,因此先對整個技術棧進行分析,總結:ios
根據以上劃分決定選用如下第三方庫和工具構成項目的完整技術棧:git
針對以上分析,完善後的項目結構如圖:
React應用開發目前已經有諸多調試工具,經常使用的如redux-devtools,Reactron等。
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 = compose
if (__DEV__) {
// 開發環境,開啓redux-devtools
const composeWithDevToolsExtension = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__
if (typeof composeWithDevToolsExtension === 'function') {
// 支持redux開發工具拓展的組合函數
composeEnhancers = composeWithDevToolsExtension
}
}
// create store
const store = createStore(
combineReducers(...),
initialState,
// 組合redux中間價和增強器,強化redux
composeEnhancers(
applyMiddleware(...middleware),
...enhancers
)
)
複製代碼
Reactotron是一款跨平臺調試React及React Native應用的桌面應用,能動態實時監測並輸出React應用等redux,action,saga異步請求等信息,如圖:
首先安裝:
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
方法拓展入口組件:
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 mode
export default DebugConfig.useReactotron
? console.tron.overlay(App)
: App
複製代碼
至此就可使用Reactotron客戶端捕獲應用中發起的全部的redux和action了。
React組件化開發原則是組件負責渲染UI,組件不一樣狀態對應不一樣UI,一般遵循如下組件設計思路:
展現型組件 | 容器組件 | |
---|---|---|
目標 | UI展現 (HTML結構和樣式) | 業務邏輯(獲取數據,更新狀態) |
感知Redux | 無 | 有 |
數據來源 | props | 訂閱Redux store |
變動數據 | 調用props傳遞的回調函數 | Dispatch Redux actions |
可重用 | 獨立性強 | 業務耦合度高 |
如今的任何大型web應用若是少了狀態管理容器,那這個應用就缺乏了時代特徵,可選的庫諸如mobx,redux等,實際上大同小異,各取所需,以redux爲例,redux是最經常使用的React應用狀態容器庫,對於React Native應用也適用。
Redux是一個JavaScript應用的可預測狀態管理容器,它不依賴於具體框架或類庫,因此它在多平臺的應用開發中有着一致的開發方式和效率,另外它還能幫咱們輕鬆的實現時間旅行,即action的回放。
Redux中間件,和Node中間件同樣,它能夠在action分發至任務處理reducer以前作一些額外工做,dispatch發佈的action將依次傳遞給全部中間件,最終到達reducer,因此咱們使用中間件能夠拓展諸如記錄日誌,添加監控,切換路由等功能,因此中間件本質上只是拓展了store.dispatch
方法。
有些時候咱們可能並不知足於拓展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
方法
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分析。
使用redux提供的createStore
方法建立redux store,可是在實際項目中咱們經常須要拓展redux添加某些自定義功能或服務,如添加redux中間件,添加異步任務管理saga,加強redux等:
// creates the store
export 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默認提供了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 tree
const 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 web單頁面應用中,頁面級UI組件的展現和切換徹底由路由控制,每個路由都有對應的URL及路由信息,咱們能夠經過路由統一高效的管理咱們的組件切換,保持UI與URL同步,保證應用的穩定性及友好體驗。
React Router是完整的React 路由解決方案,也是開發React應用最常使用的路由管理庫,只要用過它,絕對會喜歡上它的設計,它提供簡單的API,以聲明式方式實現強大的路由功能,諸如按需加載,動態路由等。
使用react-router v4版本能夠定義跨平臺的應用動態路由結構,所謂的動態路由(Dynamic Routing)即在渲染過程當中發生路由的切換,而不須要在建立應用前就配置好,這也正是其區別於靜態路由(Static Routing)所在,動態路由提升更靈活的路由組織方式,並且更方便編碼實現路由按需加載組件。
在react-router v2和v3版本中,開發React應用須要在開始渲染前就定義好完整的應用路由結構,全部的路由都須要同時初始化,才能在應用渲染後生效,會產生不少嵌套化路由,喪失了動態路由的靈活性和簡潔的按需加載編碼方式。
在react-router 2.x和3.x版本中,定義一個應用路由結構一般以下:
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')
)
複製代碼
很簡單,可是全部的路由結構都須要在渲染應用前,統必定義,層層嵌套;並且若是要實現異步按需加載還須要在這裏對路由配置對象進行修改,使用getComponent
API,並侵入改造該組件,配合webpack的異步打包加載API,實現按需加載:
getComponent
,增長路由配置對象的複雜性;<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>
)
複製代碼
相比以前版本,減小了配置化的痕跡,更凸顯了組件化的組織方式,並且在渲染組件時才實現該部分路由,而若是指望按需加載該組件,則能夠經過封裝實現一個支持異步加載組件的高階組件,將通過高階組件處理後返回的組件傳入<Route>
便可,依然遵循組件化形式:
component
,保證路由聲明的簡潔性;<Route>
做爲一個真實組件建立路由,能夠渲染;另外須要注意的是,相對於以前版本提供onEnter
, onUpdate
, onLeave
等鉤子方法API在必定程度上提升了對路由的可控性,可是實質只是覆蓋了渲染組件的生命週期方法,如今咱們能夠經過路由渲染組件的生命週期方法直接控制路由,如使用componentDidMount
或 componentWillMount
代替 onEnter
。
同時使用React-Router和Redux時,大多數狀況是正常的,可是也可能出現路由變動組件未更新的狀況,如:
connect
方法將組件鏈接至redux:connect(Home)
;Route>
組件形式:<Route component={Home} />
聲明渲染的;這是爲何呢?,由於Redux會實現組件的shouldComponentUpdate
方法,當路由變化時,該組件並無接收到props代表發生了變動,須要更新組件。
那麼如何解決問題呢?,要解決這個問題只須要簡單的使用react-router-dom
提供的withRouter
方法包裹組件:
import { withRouter } from 'react-router-dom'
export default withRouter(connect(mapStateToProps)(Home))
複製代碼
在使用Redux之後,須要遵循redux的原則:單一可信數據來源,即全部數據來源都只能是reudx store,react路由狀態也不該例外,因此須要將路由state與store state鏈接。
鏈接React Router與Redux,須要使用react-router-redux
庫,並且react-router v4版本須要指定安裝@next
版本和hsitory
庫:
yarn add react-router-redux@next
yarn add history
複製代碼
而後,在建立store時,須要實現以下配置:
建立一個history對象,對於web應用,咱們選擇browserHisotry,對應須要從history/createBrowserHistory
模塊引入createHistory
方法以建立history對象;
添加routerReducer
和routerMiddleware
中間件「,其中routerMiddleware
中間件接收history對象參數,鏈接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 actions
const middleware = routerMiddleware(history)
// Add the reducer to your store on the `router` key
// Also apply our middleware for navigating
const store = createStore(
combineReducers({
...reducers,
router: routerReducer
}),
applyMiddleware(middleware)
)
return store
複製代碼
在渲染根組件時,咱們抽象出兩個組件:
<Provider>
組件包裹,注入store;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
屬性,在組件內會調用history.listen
方法監聽瀏覽器LOCATION_CHANGE
事件,最後返回react-router
的<Router >
組件,處理做爲this.props.children
傳入的路由配置,ConnectedRouter組件內容傳送。
配置上面代碼後,就可以以dispatch action的方式觸發路由切換和組件更新了:
import { push } from 'react-router-redux'
// Now you can dispatch navigation actions from anywhere!
store.dispatch(push('/about'))
複製代碼
這個reducer所作的只是將App導航路由狀態合併入store。
咱們知道瀏覽器默認有資源的緩存功能而且提供本地持久化存儲方式如localStorage,indexDb,webSQL等,一般能夠將某些數據存儲在本地,在必定週期內,當用戶再次訪問時,直接從本地恢復數據,能夠極大提升應用啓動速度,用戶體驗更有優點,咱們可使用localStorage存儲一些數據,若是是較大量數據存儲可使用webSQL。
另外不一樣於以往的直接存儲數據,啓動應用時本地讀取而後恢復數據,對於redux應用而言,若是隻是存儲數據,那麼咱們就得爲每個reducer拓展,當再次啓動應用時去讀取持久化的數據,這是比較繁瑣並且低效的方式,是否能夠嘗試存儲reducer key,而後根據key恢復對應的持久化數據,首先註冊Rehydrate reducer,當觸發action時根據其reducer key恢復數據,而後只須要在應用啓動時分發action,這也很容易抽象成可配置的拓展服務,實際上三方庫redux-persist已經爲咱們作好了這一切。
要實現redux的持久化,包括redux store的本地持久化存儲及恢復啓動兩個過程,若是徹底本身編寫實現,代碼量比較複雜,可使用開源庫redux-persist
,它提供persistStore
和autoRehydrate
方法分別持久化本地存儲store及恢復啓動store,另外還支持自定義傳入持久化及恢復store時對store state的轉換拓展。
yarn add redux-persist
複製代碼
以下在建立store時會調用persistStore相關服務-RehydrationServices.updateReducers()
:
// configure persistStore and check reducer version number
if (ReduxPersistConfig.active) {
RehydrationServices.updateReducers(store);
}
複製代碼
該方法內實現了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
便可。
persistStore(store, [config], [callback])
複製代碼
該方法主要實現store的持久化以及分發rehydration action :
接收參數主要以下:
和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參考此處,新版本有一些更新,能夠選擇性決定使用哪一個版本,詳細請點擊查看。
前面已經提到Redux與Immutable的整合,上文使用的redux -persist默認也只能處理原生JavaScript對象的redux store state,因此須要拓展以兼容Immutable。
使用redux-persist-immutable庫能夠很容易實現兼容,所作的僅僅是使用其提供的persistStore
方法替換redux-persist所提供的方法:
import { persistStore } from 'redux-persist-immutable';
複製代碼
咱們知道持久化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()
和toJS()
轉換Js和Immutable數據結構,使用方式以下:
import immutablePersistenceTransform from '../services/ImmutablePersistenceTransform'
persistStore(store, {
transforms: [immutablePersistenceTransform]
}, startApp);
複製代碼
在項目中引入Immutable之後,須要儘可能保證如下幾點:
關於Immutable及Redux,Reselect等的實踐考驗查看以前寫的一篇文章:Immutable.js與React,Redux及reselect的實踐。
前面兩點已經在前面兩節闡述過,第三點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。
當引入Immutable.js後,對應用狀態數據結構的使用API就得遵循Immutable API,而不能再使用原生JavaScript對象,數組等的操做API了,諸如,數組解構([a, b] = [b, c]),對象拓展符(...)等,存在一些問題:
針對這些問題,社區有了seamless-immutable
可供替換選擇:
seamless-immutable
庫更輕小;最後要介紹的模塊是異步任務管理,在應用開發過程當中,最主要的異步任務就是數據HTTP請求,因此咱們講異步任務管理,主要關注在數據HTTP請求的流程管理。
本項目中使用axios做爲HTTP請求庫,axios是一個Promise格式的HTTP客戶端,選擇此庫的緣由主要有如下幾點:
redux-saga是一個致力於使應用中如數據獲取,本地緩存訪問等異步任務易於管理,高效運行,便於測試,能更好的處理異常的三方庫。
Redux-saga是一個redux中間件,它就像應用中一個單獨的進程,只負責管理異步任務,它能夠接受應用主進程的redux action以決定啓動,暫停或者是取消進程任務,它也能夠訪問redux應用store state,而後分發action。
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 saga
sagaMiddleware.run(rootSaga);
複製代碼
在項目中一般會有不少並列模塊,每一個模塊的saga流也應該是並列的,須要以多分支形式並列,redux-saga提供的fork
方法就是以新開分支的形式啓動當前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,而後遍歷數組,啓動每個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
:在AppSaga
內使用takeLatest
方法監聽REQUEST_POST_LIST
action,若短期內連續發起屢次action,則會取消前面未響應的action,只發起最後一次action;getPostListSaga
子Saga:當接收到該action時,調用getPostListSaga
,並將payload傳遞給它,getPostListSaga
是AppSaga的子級Saga,在裏面處理具體異步任務;getPostList
:getPostListSaga
會調用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就會接收進行處理了。
前面已經配置好可使用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。