系列文章: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 走起。
一不当心写了那么长,文笔又不是很好,不知有多少人看完了,但愿你们都有所收获。
其中,也有很多细节也没有细说,有疑问的就留言吧。
在学习的过程当中发现还有很多相关的知识能够扩展,应该还会有下一篇。
最后,最重要的固然是附上源码。