React應用架構設計指南

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

完整項目代碼見githubcss

我的博客html

前言

如今已經有不少腳手架工具,如create-react-app,支持一鍵建立一個React應用項目結構,很方便,可是享受方便的同時,也失去了對項目架構及技術棧完整學習的機會,並且一般腳手架建立的應用技術架構並不能徹底知足咱們的業務需求,須要咱們本身修改,完善,因此若是但願對項目架構有更深掌控,最好仍是從0到1理解一個項目。前端

項目結構與技術棧

咱們此次的實踐不許備使用任何腳手架,因此咱們須要本身建立每個文件,引入每個技術和三方庫,最終造成完整的應用,包括咱們選擇的完整技術棧。java

第一步,固然是建立目錄,咱們在上一篇已經弄好,若是你尚未代碼,能夠從Github獲取:node

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

生成項目結構以下圖:react

React項目初始結構

  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目錄,包括搭建項目架構,開發應用功能,還有自動化,單元測試等,本篇主要關注項目架構的搭建,而後使用技術棧實踐開發幾個模塊。webpack

技術棧

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

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

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

  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調試工具

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

React-Redux項目結構

開發調試工具

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 = 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
  )
)
  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();

  // 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,一般遵循如下組件設計思路:

  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的回放。

redux-flow

  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方法。

redux-middleware-enhancer

加強器(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 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與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 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路由

在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 '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')
)

很簡單,可是全部的路由結構都須要在渲染應用前,統必定義,層層嵌套;並且若是要實現異步按需加載還須要在這裏對路由配置對象進行修改,使用getComponentAPI,並侵入改造該組件,配合webpack的異步打包加載API,實現按需加載:

  1. 路由層層嵌套,必須在渲染應用前統一聲明;
  2. API不一樣,須要使用getComponent,增長路由配置對象的複雜性;
  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>
)

相比以前版本,減小了配置化的痕跡,更凸顯了組件化的組織方式,並且在渲染組件時才實現該部分路由,而若是指望按需加載該組件,則能夠經過封裝實現一個支持異步加載組件的高階組件,將通過高階組件處理後返回的組件傳入<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-dom'
export 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@next
yarn add history

而後,在建立store時,須要實現以下配置:

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

    點此查看更多history相關內容

  2. 添加routerReducerrouterMiddleware中間件「,其中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

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

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

配置上面代碼後,就可以以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 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 :

  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;
  3. 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方法替換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 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之後,須要儘可能保證如下幾點:

  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 saga
sagaMiddleware.run(rootSaga);

saga分流

在項目中一般會有不少並列模塊,每一個模塊的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。

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)
}
  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。

完整項目代碼見github

參考

  1. React
  2. Redux
  3. React Router v4
  4. redux-saga
  5. Redux Persist
相關文章
相關標籤/搜索