欢迎来到好多视频第237期,这次咱们来《用 redux-saga 实现异步请求》。

本期代码: https://github.com/happycasts/episode-237-demo

准备 redux 环境

首先要有一个基本能用的 redux 环境,可以直接下载我的 react-starter-v1.0.0

下载解压之后,重命名一下

mv react-starter episode-237-demo

这个环境是基于 create-react-app 的。

先装包,把项目跑起来

cd episode-237-demo
npm i
npm start

到浏览器中,访问 localhost:3000 ,点一下页面上的按钮,是可以加载出一些文章的标题的。

不过这个效果是用 redux-thunk 实现的,这里咱们改用 saga 来做。

使用 take 监听 action

卸载 thunk 的 npm 包,安装 redux-saga

npm uninstall redux-thunk
npm i redux-saga

然后代码上调整一下。

diff --git a/src/actions/index.js b/src/actions/index.js
@@ -2,13 +2,10 @@ import axios from 'axios'
 import { POSTS_URL } from '../constants/ApiConstants'
 import * as types from '../constants/ActionTypes'

-export const loadPosts = () => dispatch => {
-  axios.get(POSTS_URL).then(
-    res => {
-      dispatch({
-        type: types.LOAD_POSTS,
-        posts: res.data
-      })
-    }
-  )
+export const fetchPostsRequest = () => ({
+  type: types.FETCH_POSTS_REQUEST
+})
+
+export const fetchPosts = () => {
+  console.log('fetchPosts...执行异步操作')
 }
diff --git a/src/components/Posts.js b/src/components/Posts.js
@@ -3,10 +3,10 @@ import styled from 'styled-components'

 class Posts extends Component {
   render () {
-    const { loadPosts, posts } = this.props
+    const { fetchPostsRequest, posts } = this.props
     return (
       <Wrap>
-        <Button onClick={loadPosts}>
+        <Button onClick={fetchPostsRequest}>
           加载文章
         </Button>
         {
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -1 +1,2 @@
 export const LOAD_POSTS = 'LOAD_POSTS'
+export const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST'
diff --git a/src/containers/PostsContainer.js b/src/containers/PostsContainer.js
@@ -1,7 +1,7 @@
 import React from 'react'
 import Posts from '../components/Posts'
 import { connect } from 'react-redux'
-import { loadPosts } from '../actions'
+import { fetchPostsRequest } from '../actions'

 const PostsContainer = props => <Posts {...props} />

@@ -10,5 +10,5 @@ const mapStateToProps = state => ({
 })

 export default connect(mapStateToProps, {
-  loadPosts
+  fetchPostsRequest
 })(PostsContainer)
diff --git a/src/store/index.js b/src/store/index.js
@@ -1,8 +1,12 @@
 import { createStore, applyMiddleware } from 'redux'
 import rootReducer from '../reducers'
 import logger from 'redux-logger'
-import thunk from 'redux-thunk'
+import createSagaMiddleware from 'redux-saga'
+import mySaga from './sagas'

-let middlewares = [logger, thunk]
+const sagaMiddleware = createSagaMiddleware()
+let middlewares = [logger, sagaMiddleware]

 export default createStore(rootReducer, applyMiddleware(...middlewares))
+
+sagaMiddleware.run(mySaga)
diff --git a/src/store/sagas.js b/src/store/sagas.js
@@ -0,0 +1,10 @@
+import { takeLatest } from 'redux-saga/effects'
+import * as types from '../constants/ActionTypes'
+
+import { fetchPosts } from '../actions'
+
+function * mySaga () {
+  yield takeLatest(types.FETCH_POSTS_REQUEST, fetchPosts)
+}
+
+export default mySaga

创建 store/sagas.js 文件,先从 redux-saga/effects 中导入 takeLastest ,然后导入常量文件中的 action 类型定义。从 action 创建器文件中导入 fetchPosts 函数。创建一个新的 generator 函数叫做 mySaga ,然后 yield takeLatest 传入两个参数,分别是 action 类型,和 fetchPosts 函数。导出 mySaga 。

takeLastest 的作用就是等待监听 store 中的 action ,一旦 FETCH_POSTS_REQUEST 这个 action 发出后,fetchPosts 才会开始执行。takeLastest 中的 Latest 表示最新的,如果 fetchPosts 在执行过程中,takeLastest 再次接收到了 FETCH_POSTS_REQUEST ,那么它就会取消正在进行的操作,而去响应最近的这一次 ,这就是 takeLastest 这个名字的由来,中文直接翻译过来就是“使用最近的一次”。

下面要连接 sagas 文件的内容到 redux store。

先到 store/index.js 中。导入 createSagaMiddleware ,然后导入 mySaga . 下面初始化 sagaMiddleware ,并加载到 store 中。最后运行 sagaMiddleware.run 来执行 mySaga 。

剩下的几个文件的修改都是要触发 FETCH_POSTS_REQUEST 这个 action 的代码。

Action 创建器文件 actions/index.js 中,把原来 thunk 思路的代码 loadPosts 函数删除。定义 fetchPostsRequest ,发出触发 saga 代码的 action 。mySaga 被执行后,会触发 fetchPosts 函数,其中会执行异步操作,但是暂时先打印一些信息,证实一下它是否能被正确触发。

PostsContainer 文件中,删除 loadPosts ,导入 fetchPostsRequest 。

Posts.js 中,点按钮的时候触发 fetchPostsRequest 。

ActionTypes 里面定义了 action 类型常量 FETCH_POSTS_REQUEST

到浏览器中,点一下“加载文章”按钮,会看到 FETCH_POSTS_REQUEST action 会被发出,然后触发 mySaga 中的 takeLastest 代码,最终执行了 fetchPosts 函数,打印出了信息。

用 call 执行异步操作

下一步来看如何执行异步操作。

git a/src/actions/index.js b/src/actions/index.js
@@ -1,11 +1,15 @@
 import axios from 'axios'
 import { POSTS_URL } from '../constants/ApiConstants'
 import * as types from '../constants/ActionTypes'
+import { call } from 'redux-saga/effects'
+
+const api = url => axios.get(url).then(res => res.data)

 export const fetchPostsRequest = () => ({
   type: types.FETCH_POSTS_REQUEST
 })

-export const fetchPosts = () => {
-  console.log('fetchPosts...执行异步操作')
+export function * fetchPosts () {
+  const posts = yield call(api, POSTS_URL)
+  console.log(posts)
 }

这次从 redux-saga/effects 中导入 call 接口,下面的 fetchPosts 要改写成一个 generator ,yield 会把 call 语句交给 redux-saga 去执行,执行期间 fetchPosts 中的语句会暂停执行。call 的一个参数 api 是执行异步请求的接口函数,暂时还没有定义,POSTS_URL 是咱们的 starter 代码中本来就定义好的一个常量,是一个公用 API 的链接,可以请求得到所有文章数据。数据返回后 call 语句执行结束,拿到的网络数据赋值给 posts ,然后才会继续执行下一句 console.log 打印出 posts 的值,这里就充分体现出来 generator 的优势了,虽然咱们这里有异步请求,但是语句写的还是跟同步时候一样。

最后来定义 api 接口,用 axios 发 get 请求给 POSTS_URL 对应的链接,最终返回请求到的数据,也就是所有博客的数据。

到浏览器中,点一下按钮,可以看到,几秒之后,posts 的数据就被打印出来了。

用 put 发出 action

数据到手,下一步就是发出 action 来修改 store 了,redux-saga 下就不用 dispatch 发 action 了,而是用 put 。

diff --git a/src/actions/index.js b/src/actions/index.js
@@ -1,7 +1,7 @@
 import axios from 'axios'
 import { POSTS_URL } from '../constants/ApiConstants'
 import * as types from '../constants/ActionTypes'
-import { call } from 'redux-saga/effects'
+import { call, put } from 'redux-saga/effects'

 const api = url => axios.get(url).then(res => res.data)

@@ -11,5 +11,5 @@ export const fetchPostsRequest = () => ({

 export function * fetchPosts () {
   const posts = yield call(api, POSTS_URL)
-  console.log(posts)
+  yield put({ type: types.FETCH_POSTS_SUCCESS, posts })
 }
diff --git a/src/constants/ActionTypes.js b/src/constants/ActionTypes.js
@@ -1,2 +1,2 @@
-export const LOAD_POSTS = 'LOAD_POSTS'
+export const FETCH_POSTS_SUCCESS = 'FETCH_POSTS_SUCCESS'
 export const FETCH_POSTS_REQUEST = 'FETCH_POSTS_REQUEST'
diff --git a/src/reducers/post.js b/src/reducers/post.js
index 04e2b01..1322292 100755
--- a/src/reducers/post.js
+++ b/src/reducers/post.js
@@ -3,7 +3,7 @@ import * as types from '../constants/ActionTypes'

 const all = (state = [], action) => {
   switch (action.type) {
-    case types.LOAD_POSTS:
+    case types.FETCH_POSTS_SUCCESS:
       return action.posts
     default:
       return state

到 actions/index.js 中,从 redux-saga/effects 中再导出 put 接口,generator 函数中把 put 语句 yield 也就是上交给 redux-saga 执行,saga 会吧 put 参数中的 action ,dispatch 出来。发出的 action 类型是 FETCH_POSTS_SUCCESS ,负载数据是包含所有文章信息的数据。

到 ActionTypes.js 中,原来的 LOAD_POSTS 就不要了,改为 FETCH_POSTS_SUCCESS

到 reudcers/posts.js 中,改一下 action 类型名即可。

浏览器中,点一下按钮,稍后会看到文章已经成功加载了。

那咱们这期《用 redux-saga 实现异步请求》的任务也就完成了。