美文网首页从零到一搭建 React 项目
从零到一搭建 react 项目系列之(十一)

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

作者: 越前君 | 来源:发表于2020-09-06 22:37 被阅读0次

    之前文章提过,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 redux-thunk@2.3.0
    
    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 (
          <div>
            <h3>About Component!</h3>
            <h5>Get User: {this.props.user.status || ''}</h5>
            {/* 我们发现这里并不是传了一个标准的 Action 对象,而是一个函数 */}
            <button onClick={() => { this.props.dispatch(getUser(false)) }}>Get User Fail</button>
            <button onClick={() => { this.props.dispatch(getUser(true)) }}>Get User Success</button>
          </div>
        )
      }
    }
    
    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 有:

    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 的逻辑了。

    <div>
      <h3>About Component!</h3>
      <h5>Get User: {this.props.user.name || ''}</h5>
      <button onClick={() => { this.props.dispatch({ type: 'FETCH_REQUEST', status: 'requesting' }) }}>Fetch Data</button>
    </div>
    

    看结果:


    🎉

    至此

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

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

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

    参考

    相关文章

      网友评论

        本文标题:从零到一搭建 react 项目系列之(十一)

        本文链接:https://www.haomeiwen.com/subject/lrkcektx.html