Redux 進階
系列文章:
Redux 進階(本文)
在之前的中,我們已經瞭解了 Redux 到底是什麼,用來處理什麼樣的問題,並建立了一個簡單的 。但是,我們同樣遺留了一些問題沒有處理,比如:非同步處理、中介軟體、模板繫結等,這些問題我們將在這篇文章中透過一個簡單的天氣預報 Demo 來一一梳理()。
Demo preview
在開始新的內容之前,先快速回顧一下的內容。
Action, Reducer & Store
建立一個基於 Redux 狀態管理的應用時,我們還是從建立 Redux 的核心開始。
首先,建立 Action。假設,發出請求和收到請求之間有一個 loading 的狀態,那麼,我們將查詢天氣這個行為劃分為 2 個 action,併為此建立 2 個工廠函式。
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 必須是一個純函式。
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 } }
最後是 Store。
import { createStore } from 'redux'import WeatherForecastReducer from '../reducers'import actions from '../actions'let store = createStore(WeatherForecastReducer)// Log the initial stateconsole.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 編寫程式碼會有很大的幫助。
非同步請求
在上一篇文章中有提到,為了保證 reducer 的純淨,Redux 中的非同步請求都是由 action 處理。
但是,reducer 需要接收一個普通的 JS 物件,action 工廠返回一個描述事件的簡單物件,那我們的非同步方法該怎麼處理哪?這就需要我們剛才提到的中介軟體來幫忙了,新增 這個中介軟體,使我們的 action 得到增強,使得 action 不單能返回物件,還能返回函式,在這個函式中還可以發起其他的 action。
其實,redux-thunk 這個中介軟體也沒有什麼特別之處,在 的案例最後已經簡單地實現了它。
/** * 雖然,中介軟體是對 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(`{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,用的是 ;然而,本文一直是以 Angular 來寫的例子,所以,這裡就用到另一個 redux 生態圈中的專案 。它其中包含了 2 個不同的庫,ng-redux 和 ng2-redux,分別對應 Angular 1.x 和 Angular 2 兩個版本。
當然,我們這裡使用 。之前那些章節和官網講述的可能相差不大,但這部分就有所區分了。
react-redux 提供一個特殊的 React 元件 Provider
,它透過 React 特性使每個元件不用顯示地傳遞 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.jsimport 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.jsexport 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 同樣也有 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 走起。
寫在最後
一不小心寫了那麼長,文筆又不是很好,不知有多少人看完了,希望大家都有所收穫。
其中,也有不少細節也沒有細說,有疑問的就留言吧。
在學習的過程中發現還有不少相關的知識可以擴充套件,應該還會有下一篇。
最後,最重要的當然是附上。
作者:Disciple_D
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/2730/viewspace-2806902/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Redux進階(一)Redux
- Flutter redux 進階FlutterRedux
- react-redux 進階ReactRedux
- React-Redux進階(像VUEX一樣使用Redux)ReactReduxVue
- React native 專案進階(redux, redux saga, redux logger)React NativeRedux
- Redux 入坑進階 – 原始碼解析Redux原始碼
- React資料狀態管理 --- Redux,Redux-Saga以及進階DvaReactRedux
- Redux進階系列1:React+Redux專案結構最佳實踐ReduxReact
- Redux 進階 — 優雅的處理 async actionRedux
- Redux 進階 -- 編寫和使用中介軟體Redux
- Redux 進階 -- 優雅的處理 async actionRedux
- Redux 進階 – react 全家桶學習筆記(二)ReduxReact筆記
- Redux 進階 - react 全家桶學習筆記(二)ReduxReact筆記
- 前端妹紙的進階之路——redux原始碼分析前端Redux原始碼
- Redux進階系列2: 如何合理地設計StateRedux
- Redux進階系列3:如何設計action、reducer、selectorRedux
- Redux 高階 -- 原始碼分析Redux原始碼
- 做有追求的coder – Redux進階compose方法的實現與解析Redux
- 做有追求的coder - Redux進階compose方法的實現與解析Redux
- CSS高階進階CSS
- Flutter Fish Redux架構演進2.0FlutterRedux架構
- 基於 Redux + Redux Persist 進行狀態管理的 Flutter 應用示例ReduxFlutter
- 高階前端進階(五)前端
- 高階前端進階(七)前端
- 高階前端進階(三)前端
- React 進階之高階元件React元件
- ElasticSearch 進階Elasticsearch
- HBase進階
- SQL進階SQL
- JavaScript進階JavaScript
- css進階CSS
- Mongodb進階MongoDB
- Masonry進階
- vue進階Vue
- protobuf進階
- 06進階
- Typescript 高階語法進階TypeScript
- React 進階(三) 高階元件React元件