无论是React
还是Redux
,工作方式都是依靠数据驱动,在开发过程中,应用数据往往存储在数据库中,通过一个api服务器暴露出来,网页应用要获取数据,就需要与服务器进行通信。
1.React
组件访问服务器
我们先来看一下一些比较简单的场景,在一些比较简单的应用中,我们可能只需要使用react
,而不使用redux
之类的数据管理框架,这时候react
组件自身也可以担当起和服务器通信的职责。
有很多JavaScript框架都支持和服务器进行通信,例如最传统的jQuery
的$.ajax()
函数。但是既然我们都已经使用了react
,就没有必要再为了一个ajax
函数在引入jQuery
这一个框架。
一个趋势是在react
应用中使用浏览器原生支持的fetch
函数来访问网络资源,fetch
函数返回的结果是一个Promise
对象,Promise
作为一个新推出的api,能够让异步处理的代码更加简洁清晰。现代浏览器大部分都已经原生支持了fetch
函数,对于不支持fetch
的浏览器版本,也可以通过fetch
的polyfill
来增加对fetch
的支持。这个polyfill
相当于是给全局的window
对象上增加了一个fetch
函数,让这个网页中的javascript
可以直接使用fetch
函数。
1.1 React
组件访问服务器的生命周期
首先,访问服务器api是一个异步操作。而javascript
是单线程语言,不可能让主线程一直等待网络请求的结果。所以,所有对服务器的数据请求必定是一个异步操作。
但是,React
组件的渲染又是同步的,当开始渲染过程之后,不可能让组件一边渲染一边等待服务器的返回结果。
所以,我们一般这样去处理一个组件和服务器的通信:
- 步骤 1 :在装载过程中,由于组件没有获取服务器结果,就不显示结果,或者显示一个
loading
之类的提示信息。与此同时,组件发出对服务器的数据请求。 - 步骤 2:当对服务器的请求获取结果时,引发组件的一次更新过程,让组件重新绘制自己的内容,显示服务器数据。
从上面过程可以看出,为了完成一次组件和服务器之间的通信,必须要经历装载和更新过程,至少要渲染一个组件两次。
那么,在装载过程中,在什么时机发出对服务器的请求呢?
通常,我们在组件的componentDidMount
函数中做请求服务器的事情。因为当生命周期函数componentDidMount
被调用时,表明装载过程已经完成,组件需要渲染的内容已经在DOM树上出现,对服务器的请求可能依赖于已经渲染的内容,在componentDidMount
函数中发送对服务器请求是一个合适的时机。
示例代码如下:
import React,{Component} from 'react';
class Weather extends Component{
constructor(props){
super(props);
this.state={
info:null
}
}
componentDidMount(){
const apiUrl=`http://localhost:7760/service/xxx`;
fetch(apiUrl).then((resp)=>{
if(resp.status!==200) throw new Error('fail to get response with status:'+resp.status);
resp.json().then((respJson)=>{
this.setState({
info:respJson.info
});
}).catch((err)=>{
this.setState({
info:null
});
})
}).catch((err)=>{
this.setState({
info:null
});
})
}
render(){
if(!this.state.info) return <div>暂无数据</div>
const {info}=this.state;
return (
<div>{info}</div>
)
}
}
虽然fetch
现在被广为接受,但是它有一个特性一直被人诟病,那就是fetch
认为只要服务器返回一个合法的http
响应就算成功,就会调用then
提供的回调函数,即使这个http
响应的状态码是表示出错的400或者500。所以,我们在then
中,要做的第一件事就是检查传入参数resp
的status
字段,只有status
是代表成功的200的时候才继续,否则以错误处理。
当resp.status
为200时,也不能直接读取resp
的内容,因为fetch
在接收到HTTP
响应的报头部分就会调用then
,不会等到整个HTTP
响应完成,所以这时候也不能保证能读到整个HTTP
报文的JSON
格式数据。所以,resp.body
函数执行并不是返回json
内容,而是返回一个新的Promise
,又要接着用then
和catch
来处理成功和失败的情况。
1.2 React
组件访问服务器的优缺点
- 优点:简单直接,容易理解。
- 缺点:把状态存放在组件中实在不是一个很好的选择,尤其是当组件变得庞大复杂了之后。
Redux
是用来帮助管理应用状态的,应该尽量把状态存放在Redux Store
中,而不是放在React
组件中。
2. Redux
访问服务器
使用redux
访问服务器,同样要解决的是异步问题。我们这里使用Redux-thunk
来处理redux
中的异步操作。
redux
单向数据流同步操作流程
驱动redux
流程的是action
对象,每一个action
对象被派发到store
上之后,同步的被分配到所有的reducer
函数,每个reducer
都是纯函数,不会产生任何的副作用,完成数据操作之后立刻同步返回,reducer
返回的结果又被拿去更新store
上的状态数据,更新状态数据的操作会立刻被同步给监听store
状态改变的函数,从而引发react
视图组件的更新。
redux-thunk
中间件
按照redux-thunk
的想法,在redux
单向数据流中,在action
对象被reducer
函数处理之前,是插入异步功能的时机。
在redux
架构下,一个action
对象在通过store.dispatch
派发,在调用reducer
函数之前,会经过一个中间件的环节,这里就是产生异步操作的机会。
异步action
对象
当我们想要让redux
帮忙处理一个异步操作的时候,代码一样要派发一个action
对象,毕竟redux
单向数据流就是由action
对象驱动的。但是这个action
对象比较特殊,我们叫他异步action对象
。这个异步action
对象可不是一个普通的javascript
对象,而是一个函数。
如果没有redux-thunk
中间件的存在,这样一个函数类型的action
对象会被派发到各个reducer
函数,reducer
函数从这些实际上是函数的action
对象上是无法获取type
字段的,所以也做不了什么实质性的处理。
不过,由于redux-thunk
这个中间件的存在,这些action
对象根本没机会接触到reducer
函数,在中间件一层就会被redux-thunk
截获。
redux-thunk
的工作是检查action
对象是不是函数,如果不是函数就放行,完成普通的action
对象的生命周期,如果是函数,则执行这个函数,并把store
的dispatch
函数和getState
函数作为参数传递到函数中去,处理过程到此为止,不会让这个异步action
对象继续往前派发到reducer
函数。
action
对象函数中完全可以通过fetch
发起一个对服务器的异步请求,当得到服务器结果之后,通过参数dispatch
,把失败或者成功的结果当做action
对象派发出去,由于这一次派发的是一个普通的action
对象,因此不会被redux-thunk
截获,而是直接派发到reducer
,驱动store
上状态的改变。
异步操作的模式
有了redux-thunk
的帮助,我们可以使用异步action
对象来完成异步访问服务器的功能了。在此之前,我们先想一想如何设计action
类型和视图。
一个访问服务器的action
,至少要涉及三个action
类型:
- 表示异步操作已经开始的
action
类型 - 表示异步操作成功的
action
类型 - 表示异步操作失败的
action
类型
当这三种类型的action
对象被派发时,会让react
组件进入各自不同的三种状态:
- 异步操作正在进行中
- 异步操作已经成功完成
- 异步操作已经失败
下面,我们来看一下action
构造函数如何定义:
import {FETCH_STARTED,FETCH_SUCCESS,FETCH_ERROR} from './actionTypes.js';
export const fetchStarted=()=>({
type:FETCH_STARTED
});
export const fetchSuccess=(result)=>({
type:FETCH_SUCCESS,
result
});
export const fetchError=(error)=>({
type:FETCH_ERROR,
error
});
export const fetchAction=(url)=>{
return (dispatch)=>{
dispatch(fetchStarted());
fetch(url).then((resp)=>{
if(resp.status!==200) throw new Error('there is an error,resp status is :'+resp.status);
resp.json().then((respJson)=>{
dispatch(fetchSuccess(respJson.info));
}).catch((error)=>{
throw new Error('Invalid Json resp:'+error);
})
}).catch((error)=>{
dispatch(fetchError(error));
})
}
}
异步action
构造函数的模式就是 函数体内返回一个新的函数,这个新的函数可以有两个参数dispatch
和getState
。分别代表redux
唯一store
上的成员函数dispatch
和getState
。