ReactJS开发笔记

再看异步请求

2019-08-19  本文已影响0人  钢笔先生

Time: 2019-08-19

同步 vs. 异步

在同步应用中,每当action被分发后,状态就会立即发生改变。

异步则不是,需要再发生触发的时机后才更新状态。

行为 | Actions

调用异步API时有两个重要的时刻:

这两个时刻通常都会改变应用程序状态,需要改变程序状态时,需要发送正常的行为用于同步修改应用程序状态。因此,至少有三种不同的行为需要发送:

此时呢,reducers需要改变状态的isFetching,这样的话,UI就知道是时候显示一个等待图标。

这时候,reducers处理行为,可以重置isFetching,同时将新数据整合到状态中去。然后UI就可以隐藏等待图标,并展示取得的数据了。

此时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

stateaction都是对象,改变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的插件,中间件的地位可以用下面的图片表述:

异步action.png 屏幕快照 2019-08-17 下午9.41.42.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可以分发彼此的结果。

下面是完整的含fetchactions.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有竞争对手吗?

有的。

使用哪种方式取决于自身,选择一个自己喜欢的即可,也要考量一下具体任务。

连接到UI

分发异步行为和分发同步行为没有什么不同,参考前面的ToDo应用即可。

参考

https://redux.js.org/advanced/async-actions

END.

上一篇 下一篇

猜你喜欢

热点阅读