现在有很多关于redux镇最新孩子的讨论redux-saga/redux-saga。它使用生成器函数来监听/调度操作。
在我绕过它之前,我想知道使用redux-saga
而不是下面我使用redux-thunk
和async / await的方法的优点/缺点。 / p>
组件可能如下所示,像往常一样调度操作。
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...
答案 0 :(得分:417)
在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
)。即动作不断被推送到thunks,并且thunk无法控制何时停止处理这些动作。
在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
后台任务。如果authAndRefreshTokenOnExpiry
在call(authorize, {token})
来电中被阻止,它也会被取消。取消自动向下传播。
答案 1 :(得分:83)
除了图书馆作者的相当全面的答案之外,我将在生产系统中添加使用saga的经验。
Pro(使用传奇):
可测试性。由于call()返回一个纯对象,因此测试传奇非常容易。测试thunk通常需要在测试中包含mockStore。
redux-saga附带了许多有关任务的有用辅助函数。在我看来,saga的概念是为你的app创建某种后台工作者/线程,它在react redux体系结构中扮演一个缺失的部分(actionCreators和reducers必须是纯函数。)这导致了下一点。 / p>
Sagas提供独立的处理所有副作用的地方。根据我的经验,修改和管理通常比thunk动作更容易。
缺点:
生成器语法。
需要学习很多概念。
API稳定性。似乎redux-saga仍在添加功能(例如频道?),社区不是那么大。如果图书馆有一天会进行非向后兼容的更新,则会有一个问题。
答案 2 :(得分:22)
我只想根据我的个人经历添加一些评论(使用传奇和thunk):
Sagas非常适合测试:
Sagas更强大。您可以在一个thunk的动作创建者中执行所有操作,您也可以在一个传奇中执行,但反之亦然(或者至少不容易)。例如:
take
)cancel
,takeLatest
,race
)take
,takeEvery
,...)Sagas还提供了其他有用的功能,它们概括了一些常见的应用程序模式:
channels
收听外部事件来源(例如websockets)fork
,spawn
)答案 3 :(得分:2)
暴徒对阵萨加斯
Redux-Thunk
和Redux-Saga
在一些重要方面有所不同,它们都是Redux的中间件库(Redux中间件是用于拦截通过dispatch()方法进入商店的动作的代码)。
一个动作实际上可以是任何东西,但是如果您遵循最佳实践,那么一个动作就是一个普通的javascript对象,它带有一个type字段,以及可选的有效负载,meta和error字段。例如
const loginRequest = {
type: 'LOGIN_REQUEST',
payload: {
name: 'admin',
password: '123',
}, };
Redux-Thunk
除了调度标准动作外,Redux-Thunk
中间件还允许您调度称为thunks
的特殊功能。
Thunk(在Redux中)通常具有以下结构:
export const thunkName =
parameters =>
(dispatch, getState) => {
// Your application logic goes here
};
也就是说,thunk
是(可选)采用某些参数并返回另一个函数的函数。内部函数需要一个dispatch function
和一个getState
函数-两者都将由Redux-Thunk
中间件提供。
Redux-Saga
Redux-Saga
中间件允许您将复杂的应用程序逻辑表示为称为sagas的纯函数。从测试的角度来看,纯函数是可取的,因为它们是可预测的和可重复的,这使得它们相对易于测试。
Sagas通过称为生成器功能的特殊功能实现。这些是ES6 JavaScript
的新功能。基本上,执行过程会在您看到yield语句的任何地方跳入和跳出生成器。认为yield
语句导致生成器暂停并返回产生的值。稍后,调用方可以在yield
之后的语句处恢复生成器。
生成器函数就是这样定义的。请注意function关键字后的星号。
function* mySaga() {
// ...
}
一旦登录传奇已向Redux-Saga
注册。但随后,在第一行上执行yield
的操作将暂停传奇,直到将类型为'LOGIN_REQUEST'
的操作分派到商店。一旦发生这种情况,执行将继续。
答案 4 :(得分:0)
这是一个结合了redux-saga
和redux-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)
})
}
}
答案 5 :(得分:0)
根据我的经验回顾了几个不同的大型React / Redux项目,Sagas为开发人员提供了一种更加结构化的编写代码的方法,这种代码更容易测试,更难以出错。
是的,从一开始就有点奇怪,但是大多数开发者在一天内对它有了足够的了解。我总是告诉别人不要担心yield
开始做什么,一旦你写了几个测试,它就会出现给你。
我已经看过几个项目,其中thunk被视为来自MVC patten的控制器,这很快变成了一个不可控制的混乱。
我的建议是在你需要的地方使用Sagas A触发B类与单个事件有关的东西。对于任何可能涉及多个操作的内容,我发现编写客户中间件并使用FSA操作的元属性来触发它更简单。
答案 6 :(得分:0)
只是一些个人经验:
在编码风格和可读性方面,过去使用redux-saga的最重要优势之一是避免在redux-thunk中使用回调地狱-不再需要使用多个嵌套然后/捕获。但是现在随着async / await thunk的流行,人们也可以在使用redux-thunk时以同步方式编写异步代码,这可能被认为是redux-think的一种改进。
使用redux-saga时可能需要编写更多样板代码,尤其是在Typescript中。例如,如果要实现获取异步功能,则可以通过一个FETCH动作直接在action.js中的一个thunk单元中执行数据和错误处理。但是在redux-saga中,可能需要定义FETCH_START,FETCH_SUCCESS和FETCH_FAILURE动作及其所有相关的类型检查,因为redux-saga的功能之一就是使用这种丰富的“令牌”机制来创建效果和指令redux存储,方便测试。当然,无需使用这些操作就可以编写传奇,但这会使它类似于重击。
就文件结构而言,redux-saga在许多情况下似乎更为明确。可以在每个sagas.ts中轻松找到与异步相关的代码,但是在redux-thunk中,需要在操作中查看它。
轻松测试可能是redux-saga中的另一个加权功能。这确实很方便。但是需要澄清的一件事是redux-saga“调用”测试不会在测试中执行实际的API调用,因此,需要为在API调用之后可以使用它的步骤指定示例结果。因此,在编写redux-saga之前,最好先详细计划一个saga及其对应的sagas.spec.ts。
Redux-saga还提供了许多高级功能,例如并行运行任务,并发帮助程序(例如takeLatest / takeEvery,fork / spawn),它们比thunk更强大。
总而言之,我个人想说:在许多正常情况下和中小型应用程序中,请使用异步/等待风格的redux-thunk。这将为您节省许多样板代码/操作/ typedef,并且您无需切换许多不同的sagas.ts并维护特定的sagas树。但是,如果您开发的大型应用程序具有非常复杂的异步逻辑,并且需要并发/并行模式等功能,或者对测试和维护的需求很高(尤其是在测试驱动的开发中),那么redux-sagas可能会挽救您的生命
无论如何,redux-saga并不比redux本身困难和复杂,并且它没有所谓的陡峭学习曲线,因为它具有有限的核心概念和API。花一点时间学习redux-saga可能会在将来的一天让自己受益。
答案 7 :(得分:-1)
更简单的方法是使用redux-auto。
来自documantasion的
redux-auto简单地通过允许您创建一个返回promise的“action”函数来修复此异步问题。伴随你的“默认”功能动作逻辑。
这个想法是让每个action in a specific file。将文件中的服务器调用与“pending”,“fulfilled”和“rejected”的reducer函数共同定位。这使得处理承诺非常容易。
它还会自动将helper object(called "async")附加到您所在州的原型,允许您在用户界面中跟踪请求的转换。
答案 8 :(得分:-1)
一个快速说明。生成器是可取消的,async / await - 不是。 因此,对于问题的一个例子,它并没有真正理解要选择什么。 但是对于更复杂的流程,有时没有比使用生成器更好的解决方案。
所以,另一个想法可能是使用带有redux-thunk的发电机,但对我来说,似乎试图用方形轮子发明一辆自行车。
当然,生成器更容易测试。