使用 Redux-thunk 和 Redux-saga 实现 React 的异步基础
我们正在构建什么
入门
Redux-thunk
应用程序组件
Redux-saga
结束
最近我有很多空闲时间,所以决定稍微研究一下 React 和 Redux。如果你想用 Redux 编写易于维护的异步代码,你需要选择一个中间件,比如 redux-thunk 或 redux-saga。
我们正在构建什么
我喜欢猫,所以这个应用程序的功能是基于Cat API开发的。你可以从这里克隆/fork GitHub 代码库。
点击“获取猫咪”按钮,它会发送一个 HTTP GET 请求,返回一张随机的猫咪图片。点击“获取更多猫咪”按钮,它会返回一个包含 5 张随机猫咪图片的数组。
我知道这看起来不太美观,但我真的不想在 CSS 上浪费时间。如果您对完整的“项目”以及 CSS 文件感兴趣,请查看我上面提到的 GitHub 代码库。
该fetchCats功能将使用 redux-thunk 实现,并fetchMoreCats使用 redux-saga 编写,以便我们可以对它们进行比较。
入门
create-react-app catapi_app
我们先来安装一些依赖项。
npm i --save react-redux redux redux-logger redux-saga redux-thunk
接下来,我们需要在……中设置redux index.js。
//index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { Provider } from 'react-redux'
import thunkMiddleware from 'redux-thunk'
import { createLogger } from 'redux-logger'
import { createStore, applyMiddleware } from 'redux'
import rootReducer from './reducers/index'
const loggerMiddleware = createLogger()
const store = createStore(
rootReducer,
applyMiddleware(
thunkMiddleware,
loggerMiddleware ))
ReactDOM.render(
<Provider store={store}>
<App/>
</Provider>,
document.getElementById('root')
);
这段代码会运行失败,因为我们没有所需的参数rootReducer。所以我们继续执行其他步骤。
// ./reducers/index.js
import { combineReducers } from 'redux'
import fetchCatReducer from './fetchCatReducer'
export default combineReducers({
cats: fetchCatReducer
})
目前我们只有一个 reducer,但我喜欢使用 combineReducer,因为如果需要添加另一个 reducer,那就容易多了。
这段代码仍然会失败,因为现在我们缺少了句点fetchCatReducer。
// ./reducers/fetchCatReducer.js
const fetchCatReducer = (state = [], action) => {
switch(action.type) {
case "FETCH_CATS_SUCCESS":
return [
...action.payload,
...state
]
case "FETCH_CATS_START":
return state
case "FETCH_CATS_ERROR":
return state
default:
return state
}
}
export default fetchCatReducer
每当我们派发一个动作时,该动作就会执行fetchCatReducer,并相应地更新我们的状态。
"FETCH_CATS_SUCCESS"HTTP 请求成功,我们必须更新状态。"FETCH_CATS_START"HTTP 请求已启动,此处适合显示例如忙碌指示器之类的信息(例如加载屏幕)。"FETCH_CATS_ERROR"HTTP 请求失败。您可以显示错误信息或其他内容。
为了保持应用程序的简洁性,在发生错误"FETCH_CATS_START"或异常时,"FETCH_CATS_ERROR"我什么也不做,只是返回之前的状态。
Redux-thunk
目前,我们的应用程序什么也不做,因为我们需要一个 action creator 来触发一个 action,而这个 action 由我们的 reducer 来处理。
//./actions/fetchCats.js
/*Helper functions. remember, we have 3 action types so far,
these functions return a plain object that has a
type attribute that our reducer can handle.
in case of success request,
the action has a payload property as well.
That's the response cat from the server
that we have requested*/
const fetchCatsError = () =>{
return {type: "FETCH_CATS_ERROR"}
}
const fetchCatsStarted = () =>{
return {type: "FETCH_CATS_START"}
}
const fetchCatsSuccess = (cat) => {
return {type: "FETCH_CATS_SUCCESS", payload: cat}
}
// fetching a random cat starts now
const fetchCats = () => dispatch => {
dispatch(fetchCatsStarted())
fetch("https://api.thecatapi.com/v1/images/search",{
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY"
}
})
.then( catResponse => catResponse.json())
.then( cat => dispatch(fetchCatsSuccess(cat)) )
.catch( err => dispatch(fetchCatsError()))
}
是的,要在 CAT API 上使用这个端点,你需要一个 API 密钥。fetchCats它乍一看可能有点奇怪,但本质上是一个返回另一个带有参数的函数的函数dispatch。一旦你调用了 dispatch,控制流就会跳转到你的 reducer 来决定下一步的操作。在我们的例子中,只有在请求成功的情况下,我们才会更新应用程序状态。顺便说一下,这就是我安装的原因redux-logger。它会持续记录你的状态和操作的变化,这样更容易追踪发生了什么。
如果您更喜欢使用 Async/await 语法,那么您可以像这样实现上述函数:
const fetchCats = () => async dispatch => {
dispatch(fetchCatsStarted())
try{
const catResponse = await fetch("https://api.thecatapi.com/v1/images/search",{
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY"
}
})
const cat = await catResponse.json()
dispatch(fetchCatsSuccess(cat))
}catch(exc){
dispatch(fetchCatsError())
}
}
应用程序组件
我不想这篇文章太长,所以省略了组件的实现部分。我会展示一下它的外观App.js,如果你想查看完整的代码,可以去GitHub上找。
//./components/App.js
import React, { Component } from 'react'
import Button from './proxy/Button'
import CatList from './CatList'
import '../css/App.css'
import { connect } from 'react-redux'
import fetchCats from '../actions/fetchCats'
class App extends Component {
render() {
return (
<div className="App">
<Button className="primary" text="Fetch cats" onClick={this.props.fetchCats}/>
<Button className="secondary" text="Fetch more cats"/>
<header className="App-header">
<CatList cats={this.props.cats}/>
</header>
</div>
)
}
}
const mapStateToProps = (state, ownProps) => ({
cats: state.cats
})
export default connect(mapStateToProps, { fetchCats })(App);
Redux-saga
Redux-saga 是一个 Redux 中间件,它使我们能够轻松地使用 Redux 实现异步代码。
要初始化它,我们需要index.js稍微调整一下。
//./index.js
...
import createSagaMiddleware from 'redux-saga'
import watchFetchMoreCatsSaga from './saga/fetchMoreCats'
//init
const sagaMiddleware = createSagaMiddleware()
//run
sagaMiddleware.run(watchFetchMoreCatsSaga)
...
在saga文件夹中,创建一个名为 . 的新文件fetchMoreCats。
//./saga/fetchMoreCats
import { takeLatest, put } from "redux-saga/effects";
//Every time we dispatch an action
//that has a type property "FETCH_MORE_CATS"
// call the fetchMoreCatsSaga function
export default function* watchFetchMoreCatsSaga(){
yield takeLatest("FETCH_MORE_CATS", fetchMoreCatsSaga)
}
//query 5 cat image at the same time
function* fetchMoreCatsSaga(){
yield put({type: "FETCH_MORE_CATS_SAGA_START"})
const catResponse = yield fetch("https://api.thecatapi.com/v1/images/search?limit=5",{
headers: {
"Content-Type": "application/json",
"x-api-key": "YOUR_API_KEY"
}
})
const cats = yield catResponse.json()
yield put({type: "FETCH_MORE_CATS_SAGA_SUCCESS", payload: cats})
}
这些function*东西叫做生成器函数。如果你想了解更多信息,请点击这里。
这个函数可以用例如 `get_event()`takeLatest来代替,但 `get_event()` 的一个很棒的特性是它只取最后一个“事件”。在我们的例子中,如果我们快速点击按钮 100 次,那么我们的应用程序就会发送 100 个请求,几乎相当于对 API 进行 DDoS 攻击 :D。所以,与其每次点击按钮都禁用它,我们可以使用 `get_event()` 。takeEverytakelatesttakeLatest
如您所见,通过调用该put函数,我们可以像之前一样触发操作dispatch。因此,让我们调整我们的代码./reducers/fetchCatReducer.js以处理新的 saga 操作。
//./reducers/fetchCatReducer.js
...
case "FETCH_MORE_CATS_SAGA_SUCCESS":
return [
...action.payload,
...state
]
case "FETCH_MORE_CATS_SAGA_START":
return state
case "FETCH_MORE_CATS_SAGA_ERROR":
return state
...
生成watchFetchMoreCatsSaga器函数会不断监听"FETCH_MORE_CATS"动作并调用我们的函数fetchMoreCatsSaga。因此,为了使其正常工作,我们需要首先触发该动作。
//./actions/fetchMoreCats.js
const fetchMoreCats = () => dispatch =>{
dispatch({type: "FETCH_MORE_CATS"})
}
export default fetchMoreCats
就是这样。每次我们调用时fetchMoreCats,它都会分发,该分发{type: "FETCH_MORE_CATS"}会“调用”我们的,而我们的又会调用。watchFetchMoreCatsSagafetchMoreCatsSaga
所以我们需要将其导入fetchMoreCats到我们的程序中App.js,并在用户点击该按钮时调用它。
//App.js
...
import fetchMoreCats from '../actions/fetchMoreCats'
//put this button in the render method
<Button className="secondary" text="Fetch more cats" onClick={this.props.fetchMoreCats}/>
//we need to map that function to the props of the App
export default connect(mapStateToProps, { fetchCats, fetchMoreCats })(App);
结束
如果您想了解更多信息:Saga 文档
如果您有任何疑问,请在评论区留言或给我发邮件。
文章来源:https://dev.to/bnorbertjs/async-react-basics-with-redux-thunk--redux-saga-4af7