系列文章:css
Redux 入門html
Redux 進階(本文)react
在以前的文章中,咱們已經瞭解了 Redux 究竟是什麼,用來處理什麼樣的問題,並建立了一個簡單的 TodoMVC Demo。可是,咱們一樣遺留了一些問題沒有處理,好比:異步處理、中間件、模板綁定等,這些問題咱們將在這篇文章中經過一個簡單的天氣預報 Demo 來一一梳理(查看源碼點這裏)。github
在開始新的內容以前,先快速回顧一下上一篇的內容。npm
建立一個基於 Redux 狀態管理的應用時,咱們仍是從建立 Redux 的核心開始。json
首先,創建 Action。假設,發出請求和收到請求之間有一個 loading 的狀態,那麼,咱們將查詢天氣這個行爲劃分爲 2 個 action,併爲此建立 2 個工廠函數。redux
export const QUERY_WEATHER_TODAY = 'QUERY_WEATHER_TODAY' export const RECEIVE_WEATHER_TODAY = 'RECEIVE_WEATHER_TODAY' export function queryWeatherToday(city) { return { type: QUERY_WEATHER_TODAY, city } } export function receiveWeatherToday(weatherToday) { return { type: RECEIVE_WEATHER_TODAY, weatherToday } }
而後,爲 Action 建立相應的 Reducer,不要忘了 Reducer 必須是一個純函數。segmentfault
export default function WeatherTodayReducer(state = {}, action) { switch (action.type) { case QUERY_WEATHER_TODAY: return { load: true, city: action.city } case RECEIVE_WEATHER_TODAY: return { ...state, load: false, detail: action.weatherToday} default: return state } }
最後是 Sotre。api
import { createStore } from 'redux' import WeatherForecastReducer from '../reducers' import actions from '../actions' let store = createStore(WeatherForecastReducer) // Log the initial state console.log('init store', store.getState()) store.dispatch(actions.queryWeatherToday('shanghai')) console.log(store.getState()) store.dispatch(actions.receiveWeatherToday({})) console.log(store.getState()) export default store
啓動應用以後,就能在控制檯中看到一下的輸出。
回顧了以前的內容之後,那咱們就進入正題,來看一些新概念。
相信你們對中間件這個詞並不陌生,Redux 中的中間件和其餘的中間件略微有些不一樣。它並非對整個 Redux 進行包裝,而是對 store.dispatch
方法進行的封裝,是 action 與 reducer 之間的擴展。
Redux 官網一步一步詳細地演示了中間件產生的緣由及其演變過程,在此我就再也不多作贅述了。
中間件在真正應用中是必不可少的一環,或許你不須要寫一箇中間件,但理解它會對你運用 Redux 編寫代碼會有很大的幫助。
在上一篇文章中有提到,爲了保證 reducer 的純淨,Redux 中的異步請求都是由 action 處理。
可是,reducer 須要接收一個普通的 JS 對象,action 工廠返回一個描述事件的簡單對象,那咱們的異步方法該怎麼處理哪?這就須要咱們剛纔提到的中間件來幫忙了,添加 redux-thunk 這個中間件,使咱們的 action 獲得加強,使得 action 不單能返回對象,還能返回函數,在這個函數中還能夠發起其餘的 action。
其實,redux-thunk 這個中間件也沒有什麼特別之處,在 Redux 官網的案例最後已經簡單地實現了它。
/** * 雖然,中間件是對 store.dispatch 的封裝,但它是添加在整個 store 上 * 因此,函數能傳遞 `dispatch` 和 `getState` 做爲參數 * * redux-thunk 的邏輯就是判斷當前的 action 是否是一個函數,是就執行函數,不是就繼續傳遞 action 給下一個中間件 */ const thunk = store => next => action => typeof action === 'function' ? action(store.dispatch, store.getState) : next(action)
因而,咱們就修改一下以前的 action,給它添加一個異步請求。
export const QUERY_WEATHER_TODAY = 'QUERY_WEATHER_TODAY' export const RECEIVE_WEATHER_TODAY = 'RECEIVE_WEATHER_TODAY' const queryWeatherToday = city => ({ type: QUERY_WEATHER_TODAY, city }) const receiveWeatherToday = weatherToday => ({ type: RECEIVE_WEATHER_TODAY, weatherToday }) export function fetchWeatherToday(city) { return dispatch => { dispatch(queryWeatherToday(city)) return fetch(`http://api.openweathermap.org/data/2.5/weather?q=${city}&APPID=${CONFIG.APPID}`) .then(response => response.json()) .then(data => dispatch(receiveWeatherToday(data))) } }
既然,咱們用了中間件,那就要在 createStore 的時候裝載中間件。
import { createStore, applyMiddleware } from 'redux' import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import WeatherForecastReducer from '../reducers' import actions from '../actions' const loggerMiddleware = createLogger() const store = createStore( WeatherForecastReducer, applyMiddleware( thunkMiddleware, loggerMiddleware ) ) store.dispatch(actions.fetchWeatherToday('shanghai')) export default store
這時,再看看應用的控制檯。
OK,Redux 核心的功能咱們基本完成,咱們繼續看看如何將它同界面綁定在一塊兒。
官網的例子都是 Redux 搭配 React,用的是 react-redux;然而,本文一直是以 Angular 來寫的例子,因此,這裏就用到另外一個 redux 生態圈中的項目 angular-redux。它其中包含了 2 個不一樣的庫,ng-redux 和 ng2-redux,分別對應 Angular 1.x 和 Angular 2 兩個版本。
固然,咱們這裏使用 ng-redux。以前那些章節和官網講述的可能相差不大,但這部分就有所區分了。
react-redux 提供一個特殊的 React 組件 Provider
,它經過 React Context 特性使每一個組件不用顯示地傳遞 store 就能使用它。
ng-redux 固然不能使用這種方式,但它可使用 angular 本身的方式——依賴注入。
ng-redux 是一個 provider
,它包含了全部 Redux store 全部的 API,額外只有 2 個 API,分別是 createStoreWith
和 connect
。
其中,createStoreWith
顯而易見是用來建立一個 store,參數同 Redux 的 createStore
方法差很少,原有建立 store 的方法就用不到了,以前的 store.js 也就被合併到了應用啓動的 index.js 裏。
import angular from 'angular' import ngRedux from 'ng-redux' import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import './assets/main.css' import WeatherForecastReducer from './reducers' import Components from './components' const loggerMiddleware = createLogger() angular.module('WeatherForecastApp', [ngRedux, Components]) .config($ngReduxProvider => { $ngReduxProvider.createStoreWith( WeatherForecastReducer, [thunkMiddleware, loggerMiddleware] ) })
這樣應用的 store 就創建好了。
另外一個 API connect
的用法同 react-redux 的 connect
方法差很少,用於將 props 和 actions 綁定到 template 上。
API 簽名是 connect(mapStateToTarget, [mapDispatchToTarget])(target)
。
其中,mapStateToTarget
是一個 function
,function
的參數是 state,返回 state 的一部分,即 select;mapDispatchToTarget
能夠是對象或函數,若是是對象,那麼它的每一個屬性都必須是 actions 工廠方法,這些方法會自動地綁定到 target
對象上,也就是說,若是用以前定義好的 action,這邊就不須要作任何的修改;若是是函數,那麼這個函數會被傳遞 dispatch 做爲參數,並且這個函數須要返回一個對象,如何 dispatch action 就由你本身設定,同時這個對象的屬性也會綁定到 target
對象上。
最後的 target
就是目標對象了,也能夠是函數,若是是函數的話,前面所傳的 2 個參數會做爲 target
函數的參數。
好了,扯了這麼多概念,估計你也暈了。
Talk is sxxt,show me the code!
// query-city/controller.js import actions from '../../actions' export default class QueryCity { constructor($ngRedux, $scope) { const unsubscribe = $ngRedux.connect(null, actions)(this) $scope.$on('$destroy', unsubscribe) } } // today-weather-board/controller.js export default class TodayWeatherBoardCtrl { constructor($ngRedux, $scope) { const unsubscribe = $ngRedux.connect(this.mapStateToThis)(this); $scope.$on('$destroy', unsubscribe); } mapStateToThis(state) { return { weatherToday: state.weatherToday }; } }
這樣,controller 是否是變得很簡潔?
Weather Forecast 部分基本和以前的部分相同,惟一的一處小修改就是把 QueryCity 控制器裏添加一個方法,在方法裏調用 2 個不一樣的 action 來替換以前按鈕上直接綁定的 action。
因而,咱們的天氣預報應用就成了這樣。
一個真實的項目確定會用到路由切換,路由狀態也是應用狀態的一部分,那麼它也應當由 Redux 來統一管理。
談到 Angular 的路由,那必須提到 ui-router。那 ui-router 怎麼整合到由 Redux 管理的項目中哪?答案是:redux-ui-router。
使用 redux-ui-router 一樣也有 3 點要注意:
使用 store 來管理應用的路由狀態
使用 action 代替 $state 來觸發路由的變動
使用 state 代替 $stateParams 來做爲路由參數
記住這些就能夠動手開工了。首先,安裝依賴:
npm install angular-ui-router redux-ui-router --save
這裏有一點要注意,redux-ui-router 雖然依賴 angular-ui-router,但它不會幫你自動安裝,須要你本身額外手動安裝,雖然你項目裏不須要引入 angular-ui-router 模塊。
安裝完依賴以後,就把它引入到咱們項目中,項目的 index.js 就變爲了
import angular from 'angular' import ngRedux from 'ng-redux' import ngReduxUiRouter from 'redux-ui-router' import thunkMiddleware from 'redux-thunk' import createLogger from 'redux-logger' import './assets/main.css' import { current, forecast } from './Router' import App from './app/app' import WeatherForecastReducer from './reducers' import Components from './components' const loggerMiddleware = createLogger() angular.module('WeatherForecastApp', [ngReduxUiRouter, ngRedux, App, Components]) .config(($urlRouterProvider, $stateProvider) => { $urlRouterProvider .otherwise('/current') $stateProvider .state('current', current) .state('forecast', forecast) }) .config($ngReduxProvider => { $ngReduxProvider.createStoreWith( WeatherForecastReducer, [thunkMiddleware, loggerMiddleware, 'ngUiRouterMiddleware'] ) })
項目中只需引入 ngReduxUiRouter
模塊,而不用再引入 ui-router 模塊到應用中。ui-router 的路由聲明就不在這裏贅述了,網上的資料也是大把大把的。
接着,將 'ngUiRouterMiddleware'
添加到中間件中,這樣距離完工就只剩最後一步了。
那就是修改主 Reducer 文件,將路由的 Reducer 合併到主 Reducer中,
import { combineReducers } from 'redux' import { router } from 'redux-ui-router' import weatherToday from './WeatherToday' import weatherForecast from './WeatherForecast' export default combineReducers({ weatherToday, weatherForecast, router })
OK,大工告成。如今,若是你刷新界面就應該能看到控制檯中已經輸出了 type
爲 @@reduxUiRouter/$stateChangeStart
和 @@reduxUiRouter/$stateChangeSuccess
的 action log。此時,若是頁面上使用 ui-sref
來切換應用路由狀態的話,一樣也能看到 redux-logger 輸出的日誌。
在這個 Demo 裏,我就不直接使用 ui-sref
,而是用例子來講明剛剛提到的 3 點中的第二點:使用 action 代替 $state 來觸發路由的變動。
import { stateGo } from 'redux-ui-router' export default class NavBarCtrl { constructor($ngRedux, $scope) { const routerAction = { stateGo } const unsubscribe = $ngRedux.connect(this.mapStateToThis, routerAction)(this) $scope.$on('$destroy', unsubscribe) } mapStateToThis(state) { return { router: state.router } } }
從代碼中能夠看到,先從 redux-ui-router 裏引入了 stateGo
方法,而後經過上一節所說的模板綁定,將這個方法綁定到當前的模板上,因而在模板中就可使用 $ctrl.stateGo()
方法來跳轉路由。
那爲何說這就知足了剛剛的第二點哪?查看源碼就能夠發現,redux-ui-router 提供的 stateGo(to, params, options)
等 API 也只是個再普通不過的 action 工廠方法,返回一個特定 type 的 action。
路由的切換是在以前添加的中間件中,作了一個相似 reducer 的處理,根據不一樣的 action type 觸發不一樣的路由事件。
觸類旁通,經過模板綁定咱們能夠得到當前應用的 state。那麼,咱們一樣能夠用過調用 $ctrl.stateGo()
等方法給路由切換添加參數來作到使用 state 代替 $stateParams 來做爲路由參數。
順便說一句,redux-ui-router 彷佛尚未支持 angular-ui-router 中的 View Load Events,若是你看懂了我剛剛所說的,那麼 pr 走起。
一不當心寫了那麼長,文筆又不是很好,不知有多少人看完了,但願你們都有所收穫。
其中,也有很多細節也沒有細說,有疑問的就留言吧。
在學習的過程當中發現還有很多相關的知識能夠擴展,應該還會有下一篇。
最後,最重要的固然是附上源碼。