从零到一搭建 react 项目系列之(十一)

之前文章提过,Redux 是 Flux 架构与函数式编程结合的产物。

一、Redux Flow

Redux 的数据流大致如下:

UI ComponentActionReducerStateUI Component

Redux Flow

用户访问页面,然后通过 View 发出 Action(一个原始的 JavaScript 对象),由 Dispatcher 进行派发,Reducer(一个纯函数)接收到后进行处理并返回状态 State(存储 State 的容器叫做 Store),然后通知 View 更新页面。

对于同步且没有副作用的操作,上述数据流可以起到管理数据,从而控制视图更新的目的。

那么遇到含有副作用的操作时(比如 Ajax 异步请求),我们应该怎么做?

答案是使用中间件。

二、中间件的概念

对于中间件或者异步操作的思想,我不展开赘述,可以看一下阮一峰老师的这篇中间件与异步操作的文章。我对文中内容有多少疑惑,但又不知道怎么说,可能是我造诣不够深。

我是这样理解的,类似 redux-thunk、redux-promise、redux-saga 等中间件是帮助我们在异步操作结束后,使得 Reducer 自动执行。

其实中间件的实现是对 store.dispatch() 的改造,在发出 Action 和执行 Reducer 之间,添加了其他功能。

例如:

let next = store.dispatch
store.dispatch = function dispatchAndLog(action) {
  console.log('dispatching', action)
  next(action)
  console.log('next state', store.getState())
}

上面的代码,对 store.dispatch() 进行了重定义,在发送 Action 前后添加了打印功能,这就是中间件的雏形。

加入中间件后,Redux 的数据流大致如下:

Redux Flow With Middleware

在含副作用的 Action 与原始 Action 之间增加了中间件的处理。其中中间件的作用转换异步操作,生成原始的 Action 对象,后面的流程不变。

在此之前,其实我们已经使用到了中间件,那就是 redux-logger

三、redux-thunk

我们先看看 redux-thunk 的源码。

// redux-thunk 源码
function createThunkMiddleware(extraArgument) {
  return ({ dispatch, getState }) => (next) => (action) => {
    if (typeof action === 'function') {
      return action(dispatch, getState, extraArgument);
    }

    return next(action);
  };
}

const thunk = createThunkMiddleware();
thunk.withExtraArgument = createThunkMiddleware;

export default thunk;

对吧,看起来并不难。当 action 为函数时,就调用该函数。

下面我们写一个案例吧。

  1. 安装 redux-thunk
$ yarn add [email protected]
  1. 调整 store/index.js,引入 redux-thunk 中间件。这里我们暂时把此前 redux-saga 的配置注释掉,并改成 redux-thunk 配置。
// src/js/store/index.js
import { createStore, applyMiddleware, compose } from 'redux'
import thunkMiddleware from 'redux-thunk'
import logger from 'redux-logger'
import reducers from '../reducers'

const initialState = { count: 0, status: 'offline' }
const composeEnhancers = (typeof window !== 'undefined' && window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) || compose
const store = createStore(reducers, initialState, composeEnhancers(applyMiddleware(thunkMiddleware, logger)))

export default store
  1. 新建一个 userHelper.js 文件。这里我们不用 fetch 或者 XHR 来创建一个异步请求,而是使用 setTimeout 方法来实现一个异步请求用户数据的场景。
// src/js/utils/userHelper.js
class UserHelper {
  // 延迟函数
  delay(time) {
    return new Promise(resolve => setTimeout(resolve, time))
  }

  // 获取数据
  fetchData(status) {
    return new Promise((resolve, reject) => {
      const response = { status: 'success', response: { name: 'Frankie', age: 20 } }
      const error = { status: 'error', error: 'oops' }
      status ? resolve(response) : reject(error)
    })
  }

  // 请求用户数据(异步场景)
  async getUser(data) {
    try {
      const res = await this.fetchData(data)
      await this.delay(2000)
      return res
    } catch (e) {
      await this.delay(1000)
      throw e
    }
  }
}

export default new UserHelper()
  1. 新建一个 userActions.js,这是 redux-thunk 的关键。
// src/js/action/userActions.js
import userHelper from '../utils/userHelper'

export const getUser = (data, callback) => {
  return (dispatch, getState) => {
    dispatch({ type: 'FETCH_REQUEST', status: 'requesting' })
    userHelper.getUser(data).then(res => {
      dispatch({ type: 'FETCH_SUCCESS', ...res })
      callback && callback(res)
    }).catch(err => {
      dispatch({ type: 'FETCH_FAILURE', ...err })
    })
  }
}
  1. 修改 About 组件,需要注意的是,在这里我们传给 store.dispatch() 的不是一个原始对象(plain object),而是 getUser 函数。这就是 redux-thunk 的特点,它可让 store.dispatch() 接受一个函数作为参数,而这个函数叫做 Action Creator
// src/js/pages/about/index.js
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { getUser } from '../../actions/userActions'

class About extends Component {
  constructor(props) {
    super(props)
    this.state = {}
  }

  render() {
    return (
      

About Component!

Get User: {this.props.user.status || ''}
{/* 我们发现这里并不是传了一个标准的 Action 对象,而是一个函数 */}
) } } const mapStateToProps = state => { return { user: state.user } } export default connect(mapStateToProps)(About)

我们点击 Get User Success 按钮,即可看到如下效果。

redux-thunk 的缺点

根据源码我们知道,它仅仅帮我们执行了这个函数,而不在乎函数主体内部是什么。实际情况下,它这个函数可能要复杂得很多。再者,如果每一个异步操作都需要如此定义一个 Action Creator,显然它是不易维护的。

  • Action 的形式不统一
  • 异步操作太分散,分散在各个 Action 当中

所以本项目将不会使用它,只是因为 redux-thunk 在之前做项目的时候用过,就拿出来讲一下。

四、redux-saga

关于接下来 redux-saga 部分的内容,我认为我可能讲解得不太好,建议看文章最后的链接。

redux-saga 是一个用于管理异步操作的中间件。它通过创建 Saga 将所有的异步操作逻辑收集在一个地方集中处理,Saga 负责协调那些复杂或异步的操作,Reducer 还是负责处理 Action 和 State 的更新。

Saga 是通过 Generator 函数创建的,如果还不太熟悉 Generator 函数,请看阮一峰老师的 ES6 入门教程中对 Generator 函数的介绍。

Saga 不同于 Thunk,Thunk 是在 Action 被创建时调用,而 Saga 只会在应用启动时调用(但初始启动的 Saga 可能会动态调用其他 Saga),Saga 可以被看作是在后台运行的进程,Saga 监听发起的 Action,然后决定基于这个 Action 来做什么:是发起一个异步调用(如 fetch 请求),还是发起其他的 Action,甚至是调用其他的 Saga。

redux-saga 的世界里,所有的任务都通过 yield Effects 来完成(Effect 可以看作是 redux-saga 的任务单元)。Effects 是简单的 JavaScript 对象,包含了要被 Saga middleware 执行的信息(比如,我们 Redux Action 其实是一个包含了执行信息({ type, ... })的原始的 JavaScript 对象 ),redux-saga 为各项任务提供了各种 Effect 创建器,比如调用一个异步函数,发起一个 Action 到 Store,启动一个后台任务或者等待一个满足某些条件的未来的 Action。

redux-saga 核心 API
1. Saga 辅助函数

redux-saga 提供了一些辅助函数,用来在一些特定的 Action 被发起到 Store 时派生任务,下面先讲解两个辅助函数:takeEverytakeLatest

  • takeEvery
    例如:每次点击 Fetch 按钮是,我们发起一个 FETCH_REQUESTED 的 Action。我们想通过启动一个任务从服务器获取一些数据来处理这个 Action。
// src/
import { call, put, takeEvery } from 'redux-saga/effects'
import userHelper from '../utils/userHelper'

// 创建一个异步任务
function* fetchData(action) {
  try {
    // call([context, fnName], ...args)
    const data = yield call([userHelper, userHelper.getUser], action.flag)
    yield put({ type: 'FETCH_SUCCESS', ...data })
  } catch (e) {
    yield put({ type: 'FETCH_FAILURE', ...e })
  }
}

// 每次 FETCH_REQUESTED Action 被发起时启动上面的任务
export function* watchFetchData(a) {
  yield takeEvery('FETCH_REQUEST', fetchData)

  // 等同于
  // while (true) {
  //   const action = yield take('FETCH_REQUEST')
  //   yield fork(fetchData, action)
  // }
}
  • takeLatest
    在上面的例子,takeEvery 允许多个 fetchData 实例同时启动,在某个特定的时刻,我们可以启动新的 fetchData 任务,尽管此前还有一个或者多个 fetchData 尚未结束。

    如果只想得到最新的那个请求的响应,我们可以使用 takeLatest 辅助函数

    和 takeEvery 不同的是,在任何时刻 takeLatest 只允许一个 fetchData 任务,并且这个任务时最后被启动的那个。如果此前已经有一个任务在执行,那么此前这个任务会自动被取消。
import { takeLatest } from 'redux-saga/effects'

export function* watchFetchData(a) {
  yield takeLatest('FETCH_REQUEST', fetchData)
}
2. Effect 创建器

Saga 是由一个个的 effect 组成的,那么 effect 是什么?

redux-saga 官网的解释:一个 effect 就是一个 Plain Object JavaScript 对象,包含一些将被 saga middleware 执行的指令。redux-saga 提供了很多 effect 创建器,如 callputtake 等。

比如 call

import { call } from 'redux-saga/effects'

function* fetchData() {
  yield call(fetch)
}

call(userHelper.getUser) 生成的就是一个 effect,类似如下:

{
  isEffect: true,
  type: 'CALL',
  fn: fetch
}

常用的 effect 有:

  • take(pattern)
  • put(action)
  • call(fn, ...args)
  • fork(fn, ...args)
  • select(selector, ...args)

3. 常用 Effect 方法

(1)take

take 这个方法是用来监听未来的 Action,它创建一个命令对象,告诉 Middleware 等待一个特定的 Action,Generator 函数会暂停,直到一个与 pattern 匹配的 action 被发起,才会继续执行下面的语句。也就是说,take 是一个阻塞的 effect。

export function* watchFetchData(a) {
  while (true) {
    // 监听一个 type 为 'FETCH_REQUEST' 的 Action 的执行,直到这个 Action被触发,
    // 才会执行下面的 yield fork(fetchData) 语句。
    yield take('FETCH_REQUEST')
    yield fork(fetchData)
  }
}
(2)put

它是用来发送 Action 的 effect,你可以简单地理解成 redux 框架中的 dispatch 函数。当 put 一个 Action 后,reducer 就会计算新的 state 并返回。put 也是阻塞的 effect。

结合 take 和 put 方法,举个例子:

// *********************** 辅助理解 ***********************

// 在 redux 中,我们发起这样一个 Action
const fetchAction = { type: 'FETCH_REQUEST' }
store.dispatch(fetchAction)

// 使用 Saga 如何处理呢?
// 需要注意的是:以下 Saga 方法实现,并不是一个完整可执行的逻辑,仅用以举例说明,辅助理解而已。
//
// 1. 首先,在我们启动 Saga 时,使用 take 来监听 type 为 'FETCH_REQUEST' 的 Action
const fetchAction = yield take('FETCH_REQUEST')
// 2. 从 UI 向 Saga 中间件传递一个 Action
this.props.dispatch({ type: 'FETCH_REQUEST' })
// 3. 此时我们的 Saga 监听到 'FETCH_REQUEST',接着开始执行 take('FETCH_REQUEST') 后面的逻辑
yield put(fetchAction)
// 4. put 方法,可以发出 Action,且发出的 Action 会被 Reducer 监听到。从而返回一个新状态
(3)call/apply
call(fn, ...args)

// 支持传递 this 上下文给 fn。在调用对象方法时很有用。
call([context, fn], ...args)

// 支持用字符串传递 fn。在调用对象的方法时很有用。
// 例如 yield call([localStorage, 'getItem'], 'redux-saga')。
call([context, fnName], ...args)

// call([context, fn], ...args) 的另一种写法
apply(context, fn, [args])

语法与 JS 中的 call/apply 相似。

可以把它简单的理解为调用其他函数的函数,它命令 middleware 以参数 args 来调用 fn 函数。

注意: fn 既可以是一个 Generator 函数, 也可以是一个返回 Promise 或任意其它值的普通函数

还有,call 是阻塞的 effect。

(4)fork
fork(fn, ...args)

fork 类似于 call,可以用来调用普通函数和 Generator 函数。不过,fork 的调用是非阻塞的,Generator 不会在等待 fn 返回结果的时候被 middleware 暂停;恰恰相反地,它在 fn 被调用时便会立即恢复执行。

(5)select
select(selector, ...args)

// 如果 select 的参数为空会取得完整的 state(与调用 getState() 的结果相同)
// yield select()

// 返回 state 的一部分数据可以这样获取
// yield select(state => state.user)

select 函数是用来指示 middleware 调用提供的选择器获取 Store 上的 state 数据。你也可以简单的把它理解为 redux 框架中获取 store 上的 state 数据一样的功能(store.getState()

4. Middleware API
  • createSagaMiddleware()
    创建一个 Redux middleware,并将 Sagas 连接到 Redux Store。

  • middleware.run(saga, ...args)
    动态地运行 saga,只能用于在 applyMiddleware 阶段之后执行 Saga。

    sagas 中的每个函数都必须返回一个 Generator 对象,middleware 会迭代这个 Generator 并执行所有 yield 后的 Effect。(Effect 可以看作是 redux-saga 的任务单元)

五、Saga 案例实现

下面写一个处理 Fetch 请求的异步处理场景。

首先,实现 Saga 处理场景:

import { call, fork, put, select, take, delay, race, takeEvery, takeLatest } from 'redux-saga/effects'

// fetch 请求
function fetch() {
  return new Promise((resolve, reject) => {
    window
      .fetch('http://192.168.1.124:7701/config')
      .then(response => response.json())
      .then(res => {
        // 请求成功,返回一个 JSON 数据:{"name":"Frankie","age":20}
        resolve(res)
      })
      .catch(err => {
        reject(err)
      })
  })
}

// saga 处理异步场景
function* fetchData() {
  try {
    // race 与 Promise.race 类似,这里做一个超时处理
    const { result, timeout } = yield race({
      result: call(fetch),
      timeout: delay(30000)
    })
    if (timeout) throw new Error('请求超时!')
    yield put({ type: 'FETCH_SUCCESS', ...result })
  } catch (e) {
    console.warn(e)
    yield put({ type: 'FETCH_FAILURE', status: 'error', error: 'oops' })
  }
}

export function* watchFetchData() {
  // 每次 Saga 监听到 'FETCH_REQUEST' 类型的 Action,都会触发 fetchData 函数
  yield takeEvery('FETCH_REQUEST', fetchData)
}

接着,我们在 UI 中派发一个 FETCH_REQUEST 的 Action,然后 Saga 监听到之后,就会执行 fetchData 的逻辑了。

About Component!

Get User: {this.props.user.name || ''}

看结果:


至此

Redux + Middleware 基本的已经介绍完了,但我不认为我讲好了。建议大家看看以下几篇文章来加深理解。

还有 Redux 搭配中间件的我认为要学习的 API 很多,有点费劲。有空看下另一个解决方案: MobX

接下来终于可以介绍 react-hot-loader 热更新了,关于 react-router、redux、react-redux、redux-saga 等内容花了好多篇幅。

参考

  • redux-saga 中文文档
  • 彻彻底底教会你使用 Redux-saga
  • redux-saga 框架使用详解及 Demo 教程

你可能感兴趣的:(从零到一搭建 react 项目系列之(十一))