现在有很多人在谈论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...

当前回答

我只是想从我的个人经验(使用sagas和thunk)中补充一些评论:

传奇故事很适合测试:

您不需要模拟用效果包装的函数 因此,测试是干净的,可读的,易于编写的 在使用saga时,动作创建者通常会返回简单的对象字面量。与thunk的承诺不同,它也更容易测试和断言。

传说更有力量。你在一个坦克动作创造者中可以做到的所有事情,你也可以在一个传奇中做到,但反之亦然(至少不容易)。例如:

等待一个/多个动作被分派(采取) 取消现有的例程(取消,taklatest, race) 多个例程可以监听相同的动作(take, takeEvery,…)

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

监听外部事件源的通道(例如websockets) Fork模型(Fork, spawn) 节气门 ...

Sagas are great and powerful tool. However with the power comes responsibility. When your application grows you can get easily lost by figuring out who is waiting for the action to be dispatched, or what everything happens when some action is being dispatched. On the other hand thunk is simpler and easier to reason about. Choosing one or another depends on many aspects like type and size of the project, what types of side effect your project must handle or dev team preference. In any case just keep your application simple and predictable.

其他回答

我只是想从我的个人经验(使用sagas和thunk)中补充一些评论:

传奇故事很适合测试:

您不需要模拟用效果包装的函数 因此,测试是干净的,可读的,易于编写的 在使用saga时,动作创建者通常会返回简单的对象字面量。与thunk的承诺不同,它也更容易测试和断言。

传说更有力量。你在一个坦克动作创造者中可以做到的所有事情,你也可以在一个传奇中做到,但反之亦然(至少不容易)。例如:

等待一个/多个动作被分派(采取) 取消现有的例程(取消,taklatest, race) 多个例程可以监听相同的动作(take, takeEvery,…)

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

监听外部事件源的通道(例如websockets) Fork模型(Fork, spawn) 节气门 ...

Sagas are great and powerful tool. However with the power comes responsibility. When your application grows you can get easily lost by figuring out who is waiting for the action to be dispatched, or what everything happens when some action is being dispatched. On the other hand thunk is simpler and easier to reason about. Choosing one or another depends on many aspects like type and size of the project, what types of side effect your project must handle or dev team preference. In any case just keep your application simple and predictable.

在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 })
)

注意,我们通过简单地将模拟的数据注入迭代器的下一个方法来模拟api调用结果。模拟数据比模拟函数简单得多。

要注意的第二件事是yield take(ACTION)的调用。动作创建者在每个新动作(例如LOGIN_REQUEST)上调用链接。也就是说,动作会不断地推送给坦克,而坦克无法控制何时停止处理这些动作。

在redux-saga中,生成器拉动下一个动作。也就是说,他们可以控制什么时候听某些动作,什么时候不听。在上面的例子中,流指令被放置在一个while(true)循环中,因此它将侦听每个传入的动作,这在某种程度上模仿了thunk push行为。

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

处理注销用户操作 在第一次成功登录时,服务器返回一个令牌,该令牌将在某个延迟中过期,存储在expires_in字段中。我们必须在每个expires_in毫秒时在后台刷新授权 考虑到在等待api调用的结果时(无论是初始登录还是刷新),用户可能会中途注销。

你怎么用坦克来实现它;同时还为整个流程提供完整的测试覆盖率?以下是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(注销)赢得比赛(即用户点击注销按钮)。竞赛将自动取消authAndRefreshTokenOnExpiry后台任务。如果authAndRefreshTokenOnExpiry在调用(authorize, {token})中被阻塞,它也会被取消。取消自动向下传播。

您可以找到上述流程的可运行演示

在我回顾了一些不同的大型React/Redux项目的经验后,Sagas为开发人员提供了一种更结构化的代码编写方式,更容易测试,也更不易出错。

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

我曾经见过一些项目,在这些项目中,坦克被当作MVC模式的控制器来处理,这很快就变成了一个不可维护的混乱。

我的建议是,当你需要A触发B类型的与单个事件相关的东西时使用saga。对于任何可能跨越许多操作的东西,我发现编写自定义中间件并使用FSA操作的元属性来触发它更简单。

一个小提示。生成器是可取消的,async/await - not。 举个例子,选什么并没有什么意义。 但对于更复杂的流,有时没有比使用生成器更好的解决方案了。

所以,另一个想法可能是使用带有还原坦克的发电机,但对我来说,这就像试图发明一辆有方轮子的自行车。

当然,生成器更容易测试。

除了图书馆作者相当彻底的回答之外,我将添加我在生产系统中使用saga的经验。

优点(使用saga):

Testability. It's very easy to test sagas as call() returns a pure object. Testing thunks normally requires you to include a mockStore inside your test. redux-saga comes with lots of useful helper functions about tasks. It seems to me that the concept of saga is to create some kind of background worker/thread for your app, which act as a missing piece in react redux architecture(actionCreators and reducers must be pure functions.) Which leads to next point. Sagas offer independent place to handle all side effects. It is usually easier to modify and manage than thunk actions in my experience.

Con:

发电机的语法。 有很多概念要学。 API的稳定性。redux-saga似乎还在添加新功能(比如Channels?),社区也没有那么大。如果库有一天进行了不向后兼容的更新,就会有问题。