首页 文章

使用带有ES6发生器的redux-saga与带有ES2017异步/等待的redux-thunk的优点/缺点

提问于
浏览
396

现在有很多关于redux镇最新孩子的讨论,redux-saga/redux-saga . 它使用生成器函数来监听/分派操作 .

在我绕过它之前,我想知道使用 redux-saga 的优缺点,而不是下面的方法,我正在使用 redux-thunk 与async / await .

组件可能看起来像这样,像往常一样调度动作 .

import { login } from 'redux/auth';

class LoginForm extends Component {

  onClick(e) {
    e.preventDefault();
    const { user, pass } = this.refs;
    this.props.dispatch(login(user.value, pass.value));
  }

  render() {
    return (<div>
        <input type="text" ref="user" />
        <input type="password" ref="pass" />
        <button onClick={::this.onClick}>Sign In</button>
    </div>);
  } 
}

export default connect((state) => ({}))(LoginForm);

然后我的行为看起来像这样:

// auth.js

import request from 'axios';
import { loadUserData } from './user';

// define constants
// define initial state
// export default reducer

export const login = (user, pass) => async (dispatch) => {
    try {
        dispatch({ type: LOGIN_REQUEST });
        let { data } = await request.post('/login', { user, pass });
        await dispatch(loadUserData(data.uid));
        dispatch({ type: LOGIN_SUCCESS, data });
    } catch(error) {
        dispatch({ type: LOGIN_ERROR, error });
    }
}

// more actions...

// user.js

import request from 'axios';

// define constants
// define initial state
// export default reducer

export const loadUserData = (uid) => async (dispatch) => {
    try {
        dispatch({ type: USERDATA_REQUEST });
        let { data } = await request.get(`/users/${uid}`);
        dispatch({ type: USERDATA_SUCCESS, data });
    } catch(error) {
        dispatch({ type: USERDATA_ERROR, error });
    }
}

// more actions...

7 回答

  • 18

    更简单的方法是使用redux-auto .

    来自文件

    redux-auto简单地通过允许您创建一个返回promise的“action”函数来修复此异步问题 . 伴随你的“默认”功能动作逻辑 .

    • 不需要其他Redux异步中间件 . 例如thunk,promise-middleware,saga

    • 轻松允许您将承诺传递给redux and have it managed for you

    • 允许您将外部服务调用与其转换位置共同定位

    • 命名文件"init.js"将在应用启动时调用一次 . 这适用于在开始时从服务器加载数据

    这个想法是每个action in a specific file . 使用"pending","fulfilled"和"rejected"的reducer函数共同定位文件中的服务器调用 . 这使得处理承诺变得非常容易 .

    它还会自动将helper object(called "async")附加到您所在州的原型,允许您在UI中跟踪请求的转换 .

  • -1

    除了库作者的相当彻底的答案之外,我将在 生产环境 系统中添加使用saga的经验 .

    Pro(使用传奇):

    • 可测试性 . 测试sagas非常容易,因为call()返回一个纯对象 . 测试thunk通常需要在测试中包含mockStore .

    • redux-saga附带了许多有关任务的有用辅助函数 . 在我看来,saga的概念是为你的应用程序创建某种后台工作者/线程,它在react redux体系结构中扮演一个缺失的部分(actionCreators和reducers必须是纯函数 . )这导致了下一点 .

    • Sagas提供独立的处理所有副作用的地方 . 根据我的经验,修改和管理通常比thunk动作更容易 .

    缺点:

    • 生成器语法 .

    • 需要学习很多概念 .

    • API稳定性 . 似乎redux-saga仍在添加功能(例如 Channels ?),社区不是那么大 . 如果库有一天会进行非向后兼容的更新,则会引起关注 .

  • 403

    我只是想从我的个人经历中添加一些评论(同时使用sagas和thunk):

    萨加斯非常适合测试:

    • 您不需要模拟包含效果的函数

    • 因此,测试干净,易读且易于编写

    • 使用传奇时,动作创建者大多返回普通对象文字 . 与thunk的承诺不同,测试和断言也更容易 .

    Sagas更强大 . 所有你可以在一个thunk的动作创建者中做的事情你也可以在一个传奇中做,但反之亦然(或者至少不容易) . 例如:

    • 等待调度操作/操作( take

    • 取消现有例程( canceltakeLatestrace

    • 多个例程可以侦听相同的操作( taketakeEvery ,...)

    Sagas还提供其他有用的功能,它们概括了一些常见的应用程序模式:

    • channels 收听外部事件来源(例如websockets)

    • 前叉模型( forkspawn

    • 油门

    • ......

    Sagas是伟大而强大的工具 . 然而,权力来自责任 . 当您的应用程序增长时,您可以通过确定谁正在等待调度操作,或者在调度某些操作时发生的一切情况轻易丢失 . 另一方面,thunk更简单,更容易推理 . 选择一个或另一个取决于许多方面,如项目的类型和大小,项目必须处理的副作用类型或开发团队偏好 . 在任何情况下,只需保持您的应用程序简单和可预测 .

  • 78

    根据我的经验回顾了几个不同的大规模React / Redux项目,Sagas为开发人员提供了一种更加结构化的编写代码的方法,这种代码更容易测试,更难以出错 .

    是的,从一开始就有点奇怪,但大多数开发者在一天内对它有了足够的了解 . 我总是告诉别人不要担心 yield 开始做什么,一旦你写了几个测试,它就会来找你 .

    我已经看到了几个项目,其中thunk被视为来自MVC模式的控制器,这很快就变成了一个不可控制的混乱 .

    我的建议是在你需要的地方使用Sagas触发B类与单个事件有关的东西 . 对于任何可能涉及多个操作的内容,我发现编写客户中间件并使用FSA操作的元属性来触发它更为简单 .

  • 0

    一个快速说明 . 发电机是可取消,异步/等待 - 不是 . 因此,对于问题的一个例子,它并没有真正理解选择什么 . 但是对于更复杂的流程,有时没有比使用生成器更好的解决方案 .

    因此,另一个想法可能是使用带有redux-thunk的发电机,但对我来说,似乎试图发明一种带方形轮的自行车 .

    当然,发电机更容易测试 .

  • -1

    在redux-saga中,相当于上面的例子

    export function* loginSaga() {
      while(true) {
        const { user, pass } = yield take(LOGIN_REQUEST)
        try {
          let { data } = yield call(request.post, '/login', { user, pass });
          yield fork(loadUserData, data.uid);
          yield put({ type: LOGIN_SUCCESS, data });
        } catch(error) {
          yield put({ type: LOGIN_ERROR, error });
        }  
      }
    }
    
    export function* loadUserData(uid) {
      try {
        yield put({ type: USERDATA_REQUEST });
        let { data } = yield call(request.get, `/users/${uid}`);
        yield put({ type: USERDATA_SUCCESS, data });
      } catch(error) {
        yield put({ type: USERDATA_ERROR, error });
      }
    }
    

    首先要注意的是我们使用 yield call(func, ...args) 形式调用api函数 . call 不执行效果,它只是创建一个像 {type: 'CALL', func, args} 这样的普通对象 . 执行被委托给redux-saga中间件,该中间件负责执行该函数并使用其结果恢复生成器 .

    主要优点是您可以使用简单的相等性检查在Redux之外测试生成器

    const iterator = loginSaga()
    
    assert.deepEqual(iterator.next().value, take(LOGIN_REQUEST))
    
    // resume the generator with some dummy action
    const mockAction = {user: '...', pass: '...'}
    assert.deepEqual(
      iterator.next(mockAction).value, 
      call(request.post, '/login', mockAction)
    )
    
    // simulate an error result
    const mockError = 'invalid user/password'
    assert.deepEqual(
      iterator.throw(mockError).value, 
      put({ type: LOGIN_ERROR, error: mockError })
    )
    

    注意我们只是通过将模拟数据注入迭代器的 next 方法来模拟api调用结果 . 模拟数据比模拟函数更简单 .

    要注意的第二件事是调用 yield take(ACTION) . 动作创建者会在每个新动作上调用Thunk(例如 LOGIN_REQUEST ) . 即行动不断被推到thunk,并且thunks无法控制何时停止处理这些行动 .

    在redux-saga中,发电机会采取下一步行动 . 即他们有权控制何时采取某些行动,何时不采取行动 . 在上面的示例中,流指令被放置在 while(true) 循环内,因此它将侦听每个传入的操作,这有点模仿了thunk推送行为 .

    拉方法允许实现复杂的控制流程 . 例如,假设我们要添加以下要求

    • 处理LOGOUT用户操作

    • 在第一次成功登录时,服务器返回一个令牌,该令牌在 expires_in 字段中存储一些延迟 . 我们必须在每个 expires_in 毫秒的后台刷新授权

    • 考虑到在等待api调用的结果(初始登录或刷新)时,用户可以在中间注销 .

    你如何用thunk实现它;同时还为整个流程提供全面的测试覆盖?以下是Sagas的外观:

    function* authorize(credentials) {
      const token = yield call(api.authorize, credentials)
      yield put( login.success(token) )
      return token
    }
    
    function* authAndRefreshTokenOnExpiry(name, password) {
      let token = yield call(authorize, {name, password})
      while(true) {
        yield call(delay, token.expires_in)
        token = yield call(authorize, {token})
      }
    }
    
    function* watchAuth() {
      while(true) {
        try {
          const {name, password} = yield take(LOGIN_REQUEST)
    
          yield race([
            take(LOGOUT),
            call(authAndRefreshTokenOnExpiry, name, password)
          ])
    
          // user logged out, next while iteration will wait for the
          // next LOGIN_REQUEST action
    
        } catch(error) {
          yield put( login.error(error) )
        }
      }
    }
    

    在上面的例子中,我们使用 race 来表达我们的并发性要求 . 如果 take(LOGOUT) 赢得比赛(即用户点击了退出按钮) . 比赛将自动取消 authAndRefreshTokenOnExpiry 后台任务 . 如果 authAndRefreshTokenOnExpirycall(authorize, {token}) 呼叫中间被阻止,它也将被取消 . 取消自动向下传播 .

    你可以找到runnable demo of the above flow

  • -1

    这是一个结合了 redux-sagaredux-thunk 的最佳部分(专业)的项目:你可以通过 dispatching 获得承诺来处理传奇的所有副作用:相应的动作:https://github.com/diegohaz/redux-saga-thunk

    class MyComponent extends React.Component {
      componentWillMount() {
        // `doSomething` dispatches an action which is handled by some saga
        this.props.doSomething().then((detail) => {
          console.log('Yaay!', detail)
        }).catch((error) => {
          console.log('Oops!', error)
        })
      }
    }
    

相关问题