再看异步请求
Time: 2019-08-19
同步 vs. 异步
在同步应用中,每当action
被分发后,状态就会立即发生改变。
异步则不是,需要再发生触发的时机后才更新状态。
行为 | Actions
调用异步API时有两个重要的时刻:
- 发起调用的时刻
- 收到响应的时刻
这两个时刻通常都会改变应用程序状态,需要改变程序状态时,需要发送正常的行为用于同步修改应用程序状态。因此,至少有三种不同的行为需要发送:
- 通知
reducers
请求开始的行为
此时呢,reducers
需要改变状态的isFetching
,这样的话,UI就知道是时候显示一个等待图标。
- 通知
reducers
请求已经成功结束
这时候,reducers
处理行为,可以重置isFetching
,同时将新数据整合到状态中去。然后UI就可以隐藏等待图标,并展示取得的数据了。
- 通知
reducers
请求失败
此时reducers
可以重置isFetching
。有些时候UI会将错误信息存储到store
并在UI上显示。
定义action
的方式
一种方式是用相同的类型,额外用status
字段标记当前行为属于哪一类,说到底,type
字段也是人为约定的。
{ type: 'FETCH_POSTS' }
{ type: 'FETCH_POSTS', status: 'error', error: 'Oops' }
{ type: 'FETCH_POSTS', status: 'success', response: { ... } }
另一种方式是,单独定义行为:
{ type: 'FETCH_POSTS_REQUEST' }
{ type: 'FETCH_POSTS_FAILURE', error: 'Oops' }
{ type: 'FETCH_POSTS_SUCCESS', response: { ... } }
这两种方式都可以,具体使用哪种都可以,约定好即可。
单独定义多种类型的话,犯错的可能性会小一些。
可以考虑使用redux-actions
包。
当前在项目中,考虑使用第二种方式。
同步Actions构造器
这个部分我们定义一个actions.js
,并在代码文件中定义行为类型和行为构造函数:
export const SELECT_SUBREDDIT = 'SELECT_SUBREDDIT'
export function selectSubreddit(subreddit) {
return {
type: SELECT_SUBREDDIT,
subreddit // 作为键,实参会作为值
}
}
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
上面定义的两个行为是处理用户交互相关的,下面再定义一些和请求相关的行为。暂时先不考虑如何分发行为,先只管定义好要用到的行为。
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
当需要获取数据时,我们会分发行为REQUEST_POSTS
。
SELECT_SUBREDDIT
是用户主动触发的获取行为,我们定义一个非用户触发的行为,可以用于预取数据等操作中。
所以不把fetch
这种行为和特定的UI事件绑定的太深是比较好的。
最后,当网络请求结束后,我们就可以分发RECEIVE_POSTS
行为了,表示收到响应结果。
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
注意,真实的APP中,也还需要定义请求失败的行为分发。
设计状态的表达格式
在实现之前,需要先设定好state
的形状。在异步代码中,需要处理的形状更多,因此我们要想清楚了再实现。
通常来说,这部分是比较让人迷惑的。因为,哪些信息能描述异步应用程序、如何组织到一个单一的树中,都是不清楚。
一般来说可以用最常用的list
来存储信息。因为Web应用常常会显示一堆列表型的信息。想清楚程序中到底有哪些列表,然后就可以考虑将这些类表单独存储到state
中,这样做的好处是,可以缓存这些信息,然后在需要的时候局部更新这些信息。
下面是一个Reddit头条的state
样例。
{
selectedSubreddit: 'frontend',
postsBySubreddit: {
frontend: {
isFetching: true,
didInvalidate: false,
items: []
},
reactjs: {
isFetching: false,
didInvalidate: false,
lastUpdated: 1439478405547,
items: [
{
id: 42,
title: 'Confusion about Flux and Relay'
},
{
id: 500,
title: 'Creating a Simple Application Using React JS and Flux Architecture'
}
]
}
}
}
考虑到嵌套情况,需要像定义数据库表一样定义state
结构。
处理行为
在考虑到网络请求之前,我们先定义好reducers
。
state
和action
都是对象,改变state
的是动作,因此还需要定义reducers
来综合两者。
(state, action) => newState
也即,reducers
接收两个参数:
- 当前状态
- 行为
下面我们新建一个文件:reducers.js
来存放会用到的reducers
。
这个reducers
定个中文名是归约器,接收当前状态和行为,决定下一个新的状态是什么。
import { combineReducers } from 'redux'
import {
SELECT_SUBREDDIT,
INVALIDATE_SUBREDDIT,
REQUEST_POSTS,
RECEIVE_POSTS
} from '../actions'
selectedSubreddit = (state = 'reactjs', action) => {
switch(action.type): {
case SELECT_SUBREDDIT:
return action.subreddit
default:
return state
}
}
posts = (state = {
isFetching: false,
didInvalidate: false,
items: []
},
action) => {
switch(action.type):
case INVALIDATE_SUBREDDIT:
return Object.assign({}, state, {
didInvalidate: true
})
case REQUEST_POSTS:
return Object.assign({}, state, {
isFetching: true,
didInvalidate: false
})
case RECEIVE_POSTS:
return Object.assign({}, state, {
isFetching: false,
didInvalidate: false,
items: action.posts,
lastUpdated: action.receivedAt
})
default:
return state
}
postsBySubreddit = (state = {}, action) => {
switch (action.type) {
case INVALIDATE_SUBREDDIT:
case RECEIVE_POSTS:
case REQUEST_POSTS:
return Object.assign({}, state, {
[action.subreddit]: posts(state[action.subreddit], action)
})
default:
return state
}
}
const rootReducer = combineReducers({
postsBySubreddit,
selectedSubreddit
})
export default rootReducer
reducers
只是函数,因此我们可以用函数组合和更高阶的函数来组织它们。
异步行为构造器
终于,我们要来到了本文的主题:异步。
先把问题抛出来:我们要如何使用预先定义好的同步行为,并结合网络请求一块使用呢?
标准的方式是用redux-thunk
中间件。关于中间件是什么,简单说就是redux
的插件,中间件的地位可以用下面的图片表述:
![](https://img.haomeiwen.com/i3280225/cbf17bc1bc4a516c.png)
![](https://img.haomeiwen.com/i3280225/a8733792d5753b7b.png)
上面是标准的异步请求加中间件的方式,下面是显示副作用的方式。
通过使用这种中间件,action creator
可以返回一个函数而不是一个action object
。一般行为构造器返回的是一个对象,然后交给reducers
更新state
。
这种方式下,action creator
就变成了一个thunk
。
当函数构造器返回的是函数,这个函数就会交给Redux Thunk这个中间件。
注意,这个函数不必是纯函数,即允许有副作用,包括执行异步请求。同时,这个函数还可以分发行为,比如我们在同步行为构造器中定义的行为。
可以在actions.js
中定义特殊的thunk action creator
。
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
requestPosts = (subreddit) => ({
type: REQUEST_POSTS,
subreddit
})
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
receivePosts = (subreddit, json) => ({
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
})
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
invalidateSubreddit = (subreddit) => ({
type: INVALIDATE_SUBREDDIT,
subreddit
})
// 第一个thunk action creator: 返回的是函数
// 内部看起来不同,但是和同步的action creator用起来差不多
export function fetchPosts(subreddit) {
return function(dispatch) {
// 第一个分发:更新状态以告知API调用开始了
dispatch(requestPosts(subreddit))
// 被thunk中间件调用的函数可以返回一个值,会作为dispatch方法的返回值
// 这里返回一个promise
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(
response => response.json(),
// 不要使用catch
error => console.log('An error occurred.', error)
)
.then( // 可分发多次,这里更新接收数据后的state
json => dispatch(receivePosts(subreddit, json))
)
}
}
现在我们只是定义了能用在redux-thunk
中的异步行为构造器,返回的是函数,那么如何使用中间件呢?下面揭晓。
其实只需要用applyMiddleware()
函数来加强store
即可。
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import { selectSubreddit, fetchPosts } from './actions'
import rootReducer from './reducers'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware, // 允许我们使用dispatch方法
loggerMiddleware // 记录行为的中间件
)
)
store.dispatch(selectSubreddit('reactjs'))
store.dispatch(fetchPosts('reactjs')).then(() => console.log(store.getState()))
thunks
可以分发彼此的结果。
下面是完整的含fetch
的actions.js
:
import fetch from 'cross-fetch'
export const REQUEST_POSTS = 'REQUEST_POSTS'
function requestPosts(subreddit) {
return {
type: REQUEST_POSTS,
subreddit
}
}
export const RECEIVE_POSTS = 'RECEIVE_POSTS'
function receivePosts(subreddit, json) {
return {
type: RECEIVE_POSTS,
subreddit,
posts: json.data.children.map(child => child.data),
receivedAt: Date.now()
}
}
export const INVALIDATE_SUBREDDIT = 'INVALIDATE_SUBREDDIT'
export function invalidateSubreddit(subreddit) {
return {
type: INVALIDATE_SUBREDDIT,
subreddit
}
}
function fetchPosts(subreddit) {
return dispatch => {
dispatch(requestPosts(subreddit))
return fetch(`https://www.reddit.com/r/${subreddit}.json`)
.then(response => response.json())
.then(json => dispatch(receivePosts(subreddit, json)))
}
}
function shouldFetchPosts(state, subreddit) {
const posts = state.postsBySubreddit[subreddit]
if (!posts) {
return true
} else if (posts.isFetching) {
return false
} else {
return posts.didInvalidate
}
}
export function fetchPostsIfNeeded(subreddit) {
// Note that the function also receives getState()
// which lets you choose what to dispatch next.
// This is useful for avoiding a network request if
// a cached value is already available.
return (dispatch, getState) => {
if (shouldFetchPosts(getState(), subreddit)) {
// Dispatch a thunk from thunk!
return dispatch(fetchPosts(subreddit))
} else {
// Let the calling code know there's nothing to wait for.
return Promise.resolve()
}
}
}
这使得我们可以逐渐写出更加复杂的异步控制流,且代码还能保持简洁。
在index.js
中我们使用:
store
.dispatch(fetchPostsIfNeeded('reactjs'))
.then(() => console.log(store.getState()))
关于服务端渲染
异步行为构造器非常方便服务端渲染。可以创建store
,分发一个异步行为构造器,该构造器能分发其他的异步行为构造器以获取数据,然后只渲染返回的Promise
。这样在渲染之前,store
已经是最新的数据了。
redux-thunk有竞争对手吗?
有的。
- 可以使用redux-promise 或者 redux-promise-middleware 来分发
Promise
而不是函数。 - 可以使用 redux-observable 来分发
Observables
. - 可以使用redux-saga 中间件来构造更加复杂的异步行为。
- 可以使用 redux-pack中间件来分发基于
Promise
的异步行为。 - 也可以自定义中间件来描述如何调用API。
使用哪种方式取决于自身,选择一个自己喜欢的即可,也要考量一下具体任务。
连接到UI
分发异步行为和分发同步行为没有什么不同,参考前面的ToDo应用即可。
参考
https://redux.js.org/advanced/async-actions
END.