likes
comments
collection
share

(29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

作者站长头像
站长
· 阅读数 3
转载请注明出处,未经同意,不可修改文章内容。

🔥🔥🔥“前端一万小时”两大明星专栏——“从零基础到轻松就业”、“前端面试刷题”,已于本月大改版,合二为一,干货满满,欢迎点击公众号菜单栏各模块了解。

涉及面试题:
1. 什么是 Redux-saga?
2. Redux-saga 的模型概念是什么?
3. 在 Redux-saga 中 call() 和 put() 之间有什么区别?
4. Redux-saga 和 Redux-thunk 之间有什么区别?

编号:[react_29]

1 安装和配置 Redux-saga

❗️注意:为了便于讲解,请将代码恢复至《Redux 进阶——② Redux 中发送异步请求获取数据》中的版本!

1️⃣在 GitHub 中找到 Redux-saga,按照文档提示进行相关的安装和配置。

1️⃣-①:安装 Redux-saga 并重启项目;

npm install --save redux-saga

(29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

1️⃣-②:打开创建 store 的代码文件(store 目录下的 index.js 文件),按照官方文档提示,进行相应配置; (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

import { createStore, applyMiddleware } from "redux"; /*
																											1️⃣-③:从 redux 中引入
																											applyMiddleware 方法。这个方法
                                                      使得我们可以使用“中间件”;
                                                       */

import reducer from "./reducer"; 

import createSagaMiddleware from "redux-saga"; /*
																							 1️⃣-④:从 redux-saga 库中引入
																							 createSagaMiddleware 方法;
                                                */

const sagaMiddleware = createSagaMiddleware(); /*
																							 1️⃣-⑤:通过 createSagaMiddleware 方法
																							 生成一个 sagaMiddleware 中间件;
                                                */

const store = createStore(
  reducer,
  
  /*
  1️⃣-⑥:当创建 store 的时候,
  需要在 store 的第二个参数里边填写 applyMiddleware(sagaMiddleware);
   */
  applyMiddleware(sagaMiddleware)
  
  /*
  1️⃣-⑦:❓❓❓可下边这行代码应该怎么放置呢?即,我们既想用 redux-saga “中间件”,
  又想用到 redux-devtools-extension 这个“扩展”来方便“调试”,应该怎样放置代码呢?
  
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
   */
);  

export default store; 

答:打开 redux-devtools-extension,查看官方文档。 (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

所以,store 目录下 index.js 文件中的代码应改写为:

// 1️⃣-⑪:将 compose 函数从 redux 中引入进来;
import { createStore, applyMiddleware, compose } from "redux";  

import reducer from "./reducer"; 

import createSagaMiddleware from "redux-saga";

const sagaMiddleware = createSagaMiddleware();

// 1️⃣-⑧:直接拷贝官方文档里的代码;
const composeEnhancers =
  /*
  ❗️1️⃣-⑨:这行代码可以注释掉,因为浏览器的应用,故 window 的 object 是肯定存在的!
  typeof window === 'object' && 
   */
  
  /*
  ❗️1️⃣-⑩:下面这行代码和之前的意思一样:
  如果 window 下边有 __REDUX_DEVTOOLS_EXTENSION__ 这个变量的话,
  就执行这个变量对应的方法 window.__REDUX_DEVTOOLS_EXTENSION__()。
  否则,
  就执行 compose 函数;
   */
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;


// 1️⃣-⑬:继续拷贝官网的代码;
const enhancer = composeEnhancers( /*
																	 1️⃣-⑭:表示将 composeEnhancers 执行后的
																	 结果赋值给 enhancer;
                                    */
  applyMiddleware(sagaMiddleware), /*
  																 1️⃣-⑮:顺便把 sagaMiddleware 通过
  																 applyMiddleware 执行一下(
                                   ❗️❗️❗️官方文档里是 ...middleware,但是我们项目代码里
                                   并没有 middleware 这个“数组”变量,有的只是 sagaMiddleware 
                                   这一个“中间件”。故,用 sagaMiddleware 替换掉 ...middleware );
                                    */
);

const store = createStore(
  reducer,
  
  /*
  1️⃣-⑯:继而,我们在创建 store 的时候,就不再使用 applyMiddleware(thunk) 
  这种语法了,故注释掉:
  applyMiddleware(thunk)
   */
  enhancer // 1️⃣-⑰:取而代之,直接将 enhancer 传递进来即可!
  
  
  /*
  1️⃣-⑫:相应地删除下面这行代码,因为 
  redux-devtools-extension 相关的代码被配置在了 1️⃣-⑨ 中;
  
  window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
  */
);  

export default store; 

1️⃣-⑱:注意看 Redux-saga 官方文档; (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

在 store 目录下的 index.js 文件里,它从外部 saga.js 文件中引入了一个 mySaga

在之前的文章《Redux 进阶——③ Redux 中间件(上):初识 Redux 中间件》中,“Redux-saga 中间件”相较于“Redux-thunk 中间件”的特别之处在于:

  • Redux-thunk 是把“异步”操作放在 Action 里;
  • Redux-saga 则是把“异步”的逻辑拆分出来,单独放在一个文件里进行管理。

1️⃣-⑲:故,我们需要在 store 目录下新建一个 sagas.js 文件; (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

创建好 sagas.js 文件后,在 store 目录下的 index.js 文件中就可以引入了;

import { createStore, applyMiddleware, compose } from "redux";  

import reducer from "./reducer"; 

import createSagaMiddleware from "redux-saga";

/*
1️⃣-⑳:从当前目录下的 sagas.js 文件中引入 todoSagas;
❗️❗️❗️之所以在不知道 sagas.js 是什么内容的情况下,就可以在这里导入 todoSagas,
这是由于 ES6 中通过 export default 语法导出的,导入时可以对其进行任意命名!
 */
import todoSagas from "./sagas"

const sagaMiddleware = createSagaMiddleware();

const composeEnhancers =
  window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ? window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__({}) : compose;

const enhancer = composeEnhancers( 
  applyMiddleware(sagaMiddleware)
);

const store = createStore(
  reducer,
  enhancer  
);  

/*
1️⃣-㉑:当使用了这个“中间件”之后,按照官方文档指示,
我们还要调用 sagaMiddleware 的方法 run,让 todoSagas 执行起来!
 */
sagaMiddleware.run(todoSagas)

export default store; 

2️⃣既然平白无故地创建了一个 sagas.js 文件,那么文件里总得有点东西,不然程序去运行什么呢?

继续查看 Redux-saga 的官方文档, sagas.js 是长成这样的: (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

当然,我们的 TodoList 项目很简单,用不了上图中那么多 API,不过必要的代码一行也不能少。打开 sagas.js 文件:


/*
2️⃣-①:sagas.js 文件要求必须使用 ES6 中“generators 函数”的写法!
这里定义一个 mySaga 函数,然后最后导出这个函数;
 */
function* mySaga() {
  
  /*
  ❗️这行代码先不管,稍后根据项目具体逻辑进行改写!
  yield takeLatest("USER_FETCH_REQUESTED", fetchUser);
   */
}

export default mySaga;

2️⃣-②:看看页面效果(代码正常运行,redux devtools 插件也正常可用); (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

2 围绕 Redux-saga 来编写代码

3️⃣打开 TodoList.js 文件:

import React, {Component} from "react";
import 'antd/dist/antd.css';
import store from "./store";

/*
3️⃣-④:在这里引入 getInitList;
❗️同时,既然本文件中的“异步”代码已移走,initListAction 也可以删除掉了!
 */
import {getInitList, getInputChangeAction, getAddItemAction, getDeleteItemAction} from "./store/actionCreators"; 

import TodoListUI from "./TodoListUI"; 

/*
❗️相应地移除本文件对 axios 的引用!
import axios from "axios";
 */

class TodoList extends Component {
  constructor(props) {
    super(props);
    
    this.state = store.getState();
    
    this.handleInputChange = this.handleInputChange.bind(this);  
    
    this.handleStoreChange = this.handleStoreChange.bind(this);  
    
    this.handleButtonClick = this.handleButtonClick.bind(this); 

    this.handleItemDelete = this.handleItemDelete.bind(this);
    
    store.subscribe(this.handleStoreChange);  
    
  }
  
  render() {
    return(
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}  
        handleInputChange={this.handleInputChange}
        handleButtonClick={this.handleButtonClick}
        handleItemDelete={this.handleItemDelete}
      />
    )
  }
  
  componentDidMount() { 
    /*
    3️⃣-②:不在这里写“异步”代码的同时,我们遵循“Redux 工作流程”
    去定义一个 action:
     */
    const action = getInitList(); /*
    															3️⃣-③:注意我们 action 是被拆分到 actionCreators
    															里返回出来的,故现在本文件顶部适当位置引入 getInitList,
                                  然后记得在 actionCreators 中去定义这个 getInitList。
                                  ❗️同时,请注意 actionTypes 也是拆分出来单独写的,故
                                  需要在 actionTypes 中去定义“常量”!
                                   */
    
    /*
    3️⃣-①:为什么要用 Redux “中间件”,
    就是为了能把“异步”和“复杂”逻辑代码放在专门的位置进行编写和管理。
    这里,我们就可以把以下 AJAX 请求数据相关的代码放到 sagas.js 中!
    axios.get("http://rap2api.taobao.org/app/mock/232799/api/todolist")  
    
    .then((res) => {  
      const data = res.data.data;  
      const action = initListAction(data);  
      store.dispatch(action);
    })
    .catch(() => {alert("error")})
     */

  } 
  
  handleInputChange(e) { 

    const action = getInputChangeAction(e.target.value)
    
    store.dispatch(action);  
  }

  handleStoreChange() { 
    
    this.setState(store.getState()); 
  }


  handleButtonClick() { 
    
    const action = getAddItemAction();  
    
    store.dispatch(action); 
  }

  handleItemDelete(index) { 

    const action = getDeleteItemAction(index);
    
    store.dispatch(action); 
  } 

}

export default TodoList;

3️⃣-⑤:先在 actionTypes.js 中定义“常量” GET_INIT_LIST

export const CHANGE_INPUT_VALUE = "change_input_value";
export const ADD_TODO_ITEM = "add_todo_item";
export const DELETE_TODO_ITEM = "delete_todo_item";
export const INIT_LIST_ACTION = "init_list_action";

// ❗️定义常量 GET_LIST_ACTION;
export const GET_INIT_LIST = "get_init_list"

3️⃣-⑥:再在 actionCreators.js 中去定义 getInitList

// 3️⃣-⑦:引入“常量”GET_INIT_LIST;
import {GET_INIT_LIST, CHANGE_INPUT_VALUE, ADD_TODO_ITEM, DELETE_TODO_ITEM, INIT_LIST_ACTION} from "./actionTypes";  

export const getInputChangeAction = (value) => ({ 
  type: CHANGE_INPUT_VALUE, 
  value  
});

export const getAddItem = () => ({
  type: ADD_TODO_ITEM
});

export const getAddItemAction = () => ({
  type: ADD_TODO_ITEM
});

export const getDeleteItemAction = (index) => ({
  type: DELETE_TODO_ITEM,
  index
})

export const initListAction = (data) => ({  
  type: INIT_LIST_ACTION,  
  data  
})

/*
3️⃣-⑧:定义 getInitList;
❗️❗️❗️注意:既然我们没有用 Redux-thunk,那么返回的 action 就还得是“对象”的形式!
*/
export const getInitList = () => ({
  type: GET_INIT_LIST
})

返回 TodoList.js 文件,action 定义好后,就应该调用 store 的 dispatch 方法,将 action 传递出去;

import React, {Component} from "react";
import 'antd/dist/antd.css';
import store from "./store";

import {getInitList, getInputChangeAction, getAddItemAction, getDeleteItemAction} from "./store/actionCreators"; 

import TodoListUI from "./TodoListUI"; 

class TodoList extends Component {
  constructor(props) {
    super(props);
    
    this.state = store.getState();
    
    this.handleInputChange = this.handleInputChange.bind(this);  
    
    this.handleStoreChange = this.handleStoreChange.bind(this);  
    
    this.handleButtonClick = this.handleButtonClick.bind(this); 

    this.handleItemDelete = this.handleItemDelete.bind(this);
    
    store.subscribe(this.handleStoreChange);  
    
  }
  
  render() {
    return(
      <TodoListUI
        inputValue={this.state.inputValue}
        list={this.state.list}  
        handleInputChange={this.handleInputChange}
        handleButtonClick={this.handleButtonClick}
        handleItemDelete={this.handleItemDelete}
      />
    )
  }
  
  componentDidMount() { 
    const action = getInitList();  
    
    /*
    ❗️❗️❗️3️⃣-⑨:将 action 传递出去;
    而这一步就是 Redux-saga 最重要的一步:
    之前,我们没用 Redux-saga 等任何“中间件”时,按照 Redux 工作流程,
    这里的 action 只会传递给 store,然后 store 再拿着之前的 state 和这里的 action 
    传递给 reducer;
    但现在,使用了 Redux-saga 后,除了和上边一样,在 reducer 中能接收到 action 外,
    sagas.js 中也能接收到 action!
     */
    store.dispatch(action);
  } 
  
  handleInputChange(e) { 

    const action = getInputChangeAction(e.target.value)
    
    store.dispatch(action);  
  }

  handleStoreChange() { 
    
    this.setState(store.getState()); 
  }


  handleButtonClick() { 
    
    const action = getAddItemAction();  
    
    store.dispatch(action); 
  }

  handleItemDelete(index) { 

    const action = getDeleteItemAction(index);
    
    store.dispatch(action); 
  } 

}

export default TodoList;

3️⃣-⑩:既然使用了 Redux-saga 后,action 可以被传递给 sagas.js 文件,那我们就可以在 sagas.js 文件中完成“异步”代码逻辑的编写; (29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

// 3️⃣-⑲:引入 put 方法;
import { takeEvery, put } from "redux-saga/effects"; /*
																									3️⃣-⑪:首先,从 redux-saga 下边的
																								  effects 里引入 takeEvery 方法;
                                                  		*/
																									
import { GET_INIT_LIST } from "./actionTypes"; /*
																							 3️⃣-⑫:我们本意就是想让“类型”为 
																							 GET_INIT_LIST 的 action 在本文件中
                                               被“捕捉”到。所以,传入进来以便使用;
                                                */

import axios from "axios";  /*❗️引入 axios;*/

import { initListAction } from "./actionCreators";  /*3️⃣-⑰:引入 initListAction;*/

function* getInitList() { /*
													3️⃣-⑭:这里就和 reducer 的感觉很像了,上一步判断了“类型”,
 													这一步就执行类型对应的“函数”。
                          ❗️所以,这里就是我们放置“异步”逻辑代码的地方!
                           */
  
  try { /*
  			🏆🏆🏆:用这种“try/catch 语法”可以很好地
        展示出“数据”获取成功和失败对应的逻辑;
         */
  	
    /*
    3️⃣-⑮:generator 函数里,作“异步”请求时,不要用 promise 语法,
    要直接采用下边这种形式(❗️里边的 yield 表示——会等待 axios 
    获取数据完毕后,再把结果存在 res 里!):
    
    (❗️这里用了 axios,那么请记得将 TodoList 里关于 axios 引入的代码移除至本文件中!)
     */
    const res = yield axios.get("http://rap2api.taobao.org/app/mock/232799/api/todolist");
    
    
    /*
    3️⃣-⑯:既然“数据”获取到了,就可以拿获取到的“数据”去改变 store 里的数据了。
    按照 Redux 工作流程,我们创建一个 action,并将 res.data.data 作为“参数”传入;
    
    ❗️注意,请一定记得将 initListAction 从 actionCreators 中引入进来!
     */
    const action = initListAction(res.data.data);
    
    
    /*
    3️⃣-⑱:按流程,这里就应该调用 store 的 dispatch 方法,把 action 传递给 store 了。
    但,本文件中显然是没有 store 的。
    基于此,Redux-saga 底层为我们封装了一个 put 方法,其效果等同于 store.dispatch,
    但 put 可以追溯调用的信息,是更优的选择!
    
    ❗️❗️❗️要用 put,请一定记得在本文件中引入 put!
     */
    yield put(action); /*
    									 ❗️这里的 yield 也是表示:让 put(action) 
                       这个 action 处理过程完成后,再进行下一步的操作!
                        */
    
  } catch(e) { //❗️这里就表示:若 AJAX 获取数据失败,应该怎么办!
    console.log("获取数据失败~")  
  }

}


function* mySaga() {
  yield takeEvery(GET_INIT_LIST, getInitList); /*
  																						 ❗️❗️❗️3️⃣-⑬:然后,按照官方文档提示,
  																						 用 yield 语法,调用 takeEvery 
                                               方法,去“捕捉每一个”传递过来的 
                                               action 的“类型”,一旦“类型”是
                                               GET_INIT_LIST,就执行 getInitList。
                                                */
}

export default mySaga;

返回页面查看效果(代码一切正常运行):

(29)Redux 进阶——⑤ Redux 中间件(下):使用 Redux-saga 中间件实现 AJAX 数据请求 | React 基础理论实操

祝好,qdywxs ♥ you!