7.2.3 异步操作的模式
您可以在百度里搜索“深入浅出React和Redux 艾草文学(www.321553.xyz)”查找最新章节!
7.2.3 异步操作的模式
有了redux-thunk的帮助,我们可以用异步action对象来完成异步的访问服务器功能了,但是在此之前,我们先想一想如何设计action类型和视图。
一个访问服务器的action,至少要涉及三个action类型:
·表示异步操作已经开始的action类型,在这个例子里,表示一个请求天气信息的API请求已经发送给服务器的状态;
·表示异步操作成功的action类型,请求天气信息的API调用获得了正确结果,就会引发这种类型的action;
·表示异步操作失败的action类型,请求天气信息的API调用任何一个环节出了错误,无论是网络错误、本地代理服务器错误或者是远程服务器返回的结果错误,都会引发这个类型的action。
当这三种类型的action对象被派发时,会让React组件进入各自不同的三种状态,如下所示。
·异步操作正在进行中;
·异步操作已经成功完成;
·异步操作已经失败。
不管网络的传输速度有多快,也不管远程服务器响应有多快,我们都不能认为“异步操作正在进行中”状态会瞬间转换为“异步操作已经成功完成”或者“异步操作已经失败”状态。前面说过,网络和远程服务器都是外部实体,是靠不住的。在开发环境下可能速度很快,所以感知不到状态转换,但在其他环境下可能会明显感觉存在延迟,所以有必要在视图上体现三种状态的区别。
作为一种模式,我们需要定义三种action类型,还要定义三种对应的状态类型。
相关代码在本书的Github代码库https://github.com/mocheng//react-and-redux的chapter-07/weather_redux目录下可以找到。
和以往的习惯一样,我们为Weather组件创建一个放置所有代码的目录weather,对外接口的文件是src/weather/index.js,把这个功能模块的内容导出,代码如下:
import * as actions from './actions.js';
import reducer from './reducer.js';
import view from './view.js';
export {actions, reducer, view};
在src/weather/actionTypes.js中定义异步操作需要的三种action类型:
export const FETCH_STARTED = 'WEATHER/FETCH_STARTED';
export const FETCH_SUCCESS = 'WEATHER/FETCH_SUCCESS';
export const FETCH_FAILURE = 'WEATHER/FETCH_FAILURE';
在src/weather/status.js文件中定义对应的三种异步操作状态:
export const LOADING = 'loading';
export const SUCCESS = 'success';
export const FAILURE = 'failure';
读者可能觉得actionTypes.js和status.js内容重复了,因为三个action类型和三个状态是一一对应的。虽然看起来代码重复,但是从语义上说,action类型只能用于action对象中,状态则是用来表示视图。为了语义清晰,还是把两者分开定义。
接下来我们看src/weather/actions.js中action构造函数如何定义,首先是三个普通的action构造函数,代码如下:
import {FETCH_STARTED, FETCH_SUCCESS, FETCH_FAILURE} from './actionTypes.js';
export const fetchWeatherStarted = () => ({
type: FETCH_STARTED
});
export const fetchWeatherSuccess = (result) => ({
type: FETCH_SUCCESS,
result
})
export const fetchWeatherFailure = (error) => ({
type: FETCH_FAILURE,
error
})
三个普通的action构造函数fetchWeatherStarted、fetchWeatherSuccess和fetchWeather-Failure没有什么特别之处,只是各自返回一个有特定type字段的普通对象,它们的作用是驱动reducer函数去改变Redux Store上weather字段的状态。
关键是随后的异步action构造函数fetchWeather,代码如下:
export const fetchWeather = (cityCode) => {
return (dispatch) => {
const apiUrl = `/data/cityinfo/${cityCode}.html`;
dispatch(fetchWeatherStarted())
fetch(apiUrl).then((response) => {
if (response.status !== 200) {
throw new Error('Fail to get response with status ' + response.status);
}
response.json().then((responseJson) => {
dispatch(fetchWeatherSuccess(responseJson.weatherinfo));
}).catch((error) => {
throw new Error('Invalid json response: ' + error)
});
}).catch((error) => {
dispatch(fetchWeatherFailure(error));
})
};
}
异步action构造函数的模式就是函数体内返回一个新的函数,这个新的函数可以有两个参数dispatch和getState,分别代表Redux唯一的Store上的成员函数dispatch和getState。这两个参数的传入是redux-thunk中间件的工作,至于redux-thunk如何实现这个功能,我们在后面关于中间件的章节会详细介绍。
在这里,我们只要知道异步action构造函数的代码基本上都是这样的套路,代码如下:
export const sampleAsyncAction = () => {
return (dispatch, getState) => {
//在这个函数里可以调用异步函数,自行决定在合适的时机通过dispatch参数
//派发出新的action对象。
}
}
在我们的例子中,异步action对象返回的新函数首先派发由fetchWeatherStarted的产生的action对象。这个action对象是一个普通action对象,所以会同步地走完单向数据流,一直走到reducer函数中,引发视图的改变。同步派发这个action对象的目的是将视图置于“有异步action还未结束”的状态,完成这个提示之后,接下来才开始真正的异步操作。
这里使用fetch来做访问服务器的操作,和前面介绍的weather_react应用中的代码几乎一样,区别只是this.setState改变组件状态的语句不见了,取而代之的是通过dispatch来派发普通的action对象。也就是说,访问服务器的异步action,最后无论成败,都要通过派发action对象改变Redux Store上的状态完结。
在fetch引发的异步操作完成之前,Redux正常工作,不会停留在fetch函数执行上,如果有其他任何action对象被派发,Redux照常处理。
我们来看一看src/weather/reducer.js中的reducer函数,代码如下:
export default (state = {status: Status.LOADING}, action) => {
switch(action.type) {
case FETCH_STARTED: {
return {status: Status.LOADING};
}
case FETCH_SUCCESS: {
return {...state, status: Status.SUCCESS, ...action.result};
}
case FETCH_FAILURE: {
return {status: Status.FAILURE};
}
default: {
return state;
}
}
}
在reducer函数中,完成了上面提到的三种action类型到三种状态类型的映射,增加一个status字段,代表的就是视图三种状态之一。
这里没有任何处理异步action对象的逻辑,因为异步action对象在中间件层就被redux-thunk拦截住了,根本没有机会走到reducer函数中来。
最后来看看src/weather/view.js中的视图,也就是React组件部分,首先是无状态组件函数,代码如下:
const Weather = ({status, cityName, weather, lowestTemp, highestTemp}) => {
switch (status) {
case Status.LOADING: {
return 天气信息请求中...;
}
case Status.SUCCESS: {
return (
{cityName} {weather} 最低气温 {lowestTemp} 最高气温 {highestTemp}
)
}
case Status.FAILURE: {
return 天气信息装载失败
}
default: {
throw new Error('unexpected status ' + status);
}
}
}
和weather_react中的例子不同,因为现在状态都是存储在Redux Store上,所以这里Weather是一个无状态组件,所有的props都是通过Redux Store状态获得。
在渲染函数中,根据三种不同的状态,显示出来的内容也不一样。当视图状态为LOADING时,表示一个对天气信息的API请求刚刚发出,还没有结果返回,这时候界面上就显示一个“天气信息请求中…”字样;当视图状态为SUCCESS时,根据状态显示对应的城市和天气信息;当视图状态为FAILURE时,显示“天气信息装载失败”。
这个组件对应的mapStateToProps函数代码如下:
const mapStateTopProps = (state) => {
const weatherData = state.weather;
return {
status: weatherData.status,
cityName: weatherData.city,
weather: weatherData.weather,
lowestTemp: weatherData.temp1,
highestTemp: weatherData.temp2
};
}
为了驱动Weather组件的action,我们另外创建一个城市选择器控件CitySelector,CitySelector很简单,也不是这个应用功能的重点,我们只需要它提供一个作为视图的React组件就可以。
在CitySelector组件中,定义了四个城市的代码,代码如下:
const CITY_CODES = {
'北京': 101010100,
'上海': 101020100,
'广州': 101280101,
'深圳': 101280601
};
CitySelector组件的render函数根据CITY_CODES的定义画出四个城市的选择器,代码如下:
render() {
return (
{
Object.keys(CITY_CODES).map(
cityName => {cityName}
)
}
);
}
}
其中使用到的onChange函数使用onSelectCity来派发出action,代码如下:
onChange(ev) {
const cityCode = ev.target.value;
this.props.onSelectCity(cityCode)
}
为了让网页初始化的时候就能够获得天气信息,在componentDidMount中派发了对应第一个城市的fetchWeatheraction对象,代码如下:
componentDidMount() {
const defaultCity = Object.keys(CITY_CODES)[0];
this.props.onSelectCity(CITY_CODES[defaultCity]);
}
CitySelector的mapDispatchToProps函数提供了名为onSelectCity的函数类型prop,代码如下:
const mapDispatchToProps = (dispatch) => {
return {
onSelectCity: (cityCode) => {
dispatch(weatherActions.fetchWeather(cityCode));
}
}
};
这个city_selector提供的视图导入了weather功能组件导出的actions,显示出北京、上海、广州、深圳四个城市的选择器,当用户选中某个城市的时候,就会派发fetchWeather构造器产生的action对象,让Weather组件去服务器获取对应城市的天气信息。
完成全部代码之后,我们在网页中就可以看到最终效果,如图7-3所示。
图7-3 天气应用最终效果
当我们选择另一个城市之后,可以看到会有短暂的显示“天气信息请求中...”,然后才显示出对应城市的天气,因为访问服务器总是会有时间延迟。
我们也可以试着关闭命令行上中断npm start这个命令,等于是关闭了代理服务器,这样对API的访问必然失败,然后切换城市,可以看到“天气信息装载失败”。 深入浅出React和Redux