redux-saga 基本简介
中间件是一种独立运行于各个框架之间的代码,以函数的形式存在,连接在一起,形成一个异步队列,可以访问请求对象和响应对象,可以对请求进行拦截处理,再将处理后的控制权向下传递,终止请求,向客户端做出响应机制,来完成对任何数据的预处理和后处理
中间件的优点在于其灵活性,使用中间件开发者可以用极少的操作就能得到一个插件,用最简单的方法就能够将新的过滤器和处理程序扩展到现有的系统上,最基础的组成部分是:中间件管理器
要实现中间件模式,最重要的实现细节是:
- 可以通过调用
use()
函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做 - 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值
- 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件
至于怎么处理传递数据,目前没有严格的规则,一般有几种方式:
- 通过添加属性和方法来增强
- 使用某种处理的结果来替换 data
- 保证原始要处理的数据不变,永远返回新的副本作为处理的结果
redux-saga
是一个用于管理应用程序 Side Effect
(异步操作) 的 library
,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易,redux-saga
是一个 redux
中间件,通过 action
从主程序启动,暂停和取消,访问完整的 state
和 dispatch action
redux-saga
使用了 ES6 的 Generator
功能,让异步的流程更易于读取,写入和测试,通过这种方式,让异步看起来更加像标准同步
redux-saga 实现原理
redux-saga
是运行在 action
发送出去请求,达到 reducer
之间的一段代码
简单来看,只要引用了 redux-saga
,就会监听每个请求,当请求发送一个 action
时,redux-saga
就可以启动、暂停和取消客户端的接口请求行为,通常用于处理请求拦截等业务
redux-saga
常用的中间件 API:
-
createSagaMiddleware(options)
用来创建一个Redux middleware
,并将Sagas
连接到redux store
上
-
options
传递给middleware
的选项列表,默认可以不用传递
-
middleware.run(saga, ...args)
用于动态运行saga
,只能用在applyMiddleware
阶段之后执行saga
-
saga
一个Generator
函数 -
args
提供给saga
的参数
redux-saga 实例应用
下面以一个案例来应用 redux-saga
相应的 API,如下创建一个 react
项目,并安装 redux
、react-redux
、redux-saga
$ npx create-react-app saga-demo
$ cd saga-demo
$ npm install redux --save
$ npm install react-redux --save
$ npm install redux-saga --save
调整下目录结构,并修改相关页面及配置信息,结构如下配置:
|-- saga-demo
|-- node_modules # 项目相关依赖包目录
|-- public # 项目静态文件目录
|-- src # 项目主要开发目录
|-- mock # 项目模拟数据目录
|-- pages # 项目页面组件目录
|-- IndexPage
|-- action.js
|-- index.css
|-- index.js
|-- reducer.js
|-- saga.js
|-- server.js
|-- state.js
|-- redux # 项目状态管理目录:管理项目的整体状态
|-- reducer.js # 汇总项目所有的 reducer
|-- saga.js # 汇总项目所有的 saga
|-- store.js # 创建项目的整体 store
|-- index.css # 项目全局样式表
|-- index.js # 项目主要入口文件
|-- reportWebVitals.js # web-vitals的库
|-- root.js # 项目容器组件文件
|-- .gitignore # 项目的 git 配置
|-- package-lock.json
|-- package.json # 项目的整体配置文件
|-- README.md # 项目的说明文档文件
这里说明一下项目目录结构,为什么要这样的设计?为了更好的定义组件化和功能模块化,通过将一个组件相关的业务功能封装到一个目录下,这样的好处是方便项目管理和后期运维工作,而项目的 src/redux
目录仅仅用来进行项目状态管理
在 src/redux
创建一个 store
并基于此引用一个 reducer.js
import { createStore, applyMiddleware } from 'redux';
import reducer from './reducer';
export default function configureStore(state) {
const createStoreWithMiddleware = applyMiddleware()(createStore);
return {
...createStoreWithMiddleware(reducer, state),
};
}
调整 index.js
和 root.js
,在 IndexPage
内通过状态管理拿到组件的状态,代码如下:
import React from 'react';
import ReactDOM from 'react-dom';
import Root from './root';
import reportWebVitals from './reportWebVitals';
import './index.css';
ReactDOM.render(<Root />, document.getElementById('root'));
reportWebVitals();
import React from 'react';
import { Provider } from 'react-redux';
import Store from './redux/store';
import IndexPage from './pages/IndexPage';
const store = Store();
function Root() {
return (
<Provider store={store}>
<IndexPage />
</Provider>
);
}
export default Root;
- 首先根据
IndexPage
页面所需要的数据,进行状态声明state.js
- 然后根据
IndexPage
页面所需要的交互,进行行为管理action.js
- 最后根据
IndexPage
页面内数据与交互,进行页面输出reducer.js
export default {
bannlist: [], // 声明菜单数据,用于存储菜单数据
testdata: '测试状态',
}
export const FETCH_GLOBE_BANN = 'FETCH_GLOBE_BANN'; // 监听初始化菜单数据请求行为
export const PUTCH_GLOBE_DATA = 'PUTCH_GLOBE_DATA'; // 渲染版面加载初始化数据更新
export const ATION_ALL_TYPE = {
'PUTCH_GLOBE_DATA': (state, data) => { return { ...state, ...data }; },
};
import { ATION_ALL_TYPE } from './action';
import initState from './state';
export default (state = initState, action) => {
if (Object.prototype.toString.call(ATION_ALL_TYPE[action.type]) === '[object Function]') {
return ATION_ALL_TYPE[action.type](state, action.payload);
}
return state;
};
一个组件映射一个 reducer
,需要将组件的 reducer
引入到 redux
内,并且给组件 reducer
命名,确保唯一性
import { combineReducers } from 'redux';
import globe from '../pages/IndexPage/reducer';
export default combineReducers({
globe,
});
修改 IndexPage
组件信息,并添加一个按钮,调用接口请求事件
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
function IndexPage(props) {
const { dispatch, testdata, bannlist } = props;
console.log(testdata); // 测试状态
const handPull = () => {
// 使用 fetch 调用接口,拿到接口数据,通过 dispatch action 更新 bannlist 数据
fetch('http://iwenwiki.com/api/blueberrypai/getIndexBanner.php')
.then(res => res.json())
.then(data => {
dispatch({ type: PUTCH_GLOBE_DATA, payload: { bannlist: data.banner } });
});
};
return (<button onClick={handPull}>点击</button>);
}
export default connect(({ globe }) => ({
testdata: globe.testdata,
bannlist: globe.bannlist
}))(IndexPage);
通过 redux-saga
的 createSagaMiddleware
API 创建一个中间件,基于中间件来处理接口和状态交互动作,在 store
内引用 saga
import { createStore, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga';
import reducer from './reducer';
export default function configureStore(state) {
const sagaMiddleware = createSagaMiddleware();
const createStoreWithMiddleware = applyMiddleware(sagaMiddleware)(createStore);
return {
...createStoreWithMiddleware(reducer, state),
runSaga: sagaMiddleware.run
};
}
在 IndexPage
组件内创建 saga.js
和 server.js
,编辑内容如下:
import { take, fork, call, put } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN, PUTCH_GLOBE_DATA } from './action';
import { getBannData } from './server';
// 页面初始化加载成功后,更新用户信息数据
function* fetchGlobeBann() {
while (true) {
const { payload, callback } = yield take(FETCH_GLOBE_BANN);
const response = yield call(getBannData);
yield put({ type: PUTCH_GLOBE_DATA, payload: { bannlist: response.banner } });
}
}
export default [
fork(fetchGlobeBann),
];
/**
* 异步调用后端声明接口:获取应用轮播图片数据
* @returns
*/
export async function getBannData() {
let api = 'http://iwenwiki.com/api/blueberrypai/getIndexBanner.php';
return await fetch(api, { method: 'GET' }).then(res => res.json()).then(data => data);
}
至此,仅仅是暴露了一个组件 saga
,还需要将组件 saga
存放到 src/redux
中引用,并在 root.js
配置引用 saga
import { all } from 'redux-saga/effects';
import WatchGlobeModal from '../pages/IndexPage/saga';
export default function* rootSaga() {
yield all([
...WatchGlobeModal,
]);
}
import React from 'react';
import { Provider } from 'react-redux';
import Store from './redux/store';
import Sagas from './redux/saga';
import IndexPage from './pages/IndexPage';
const store = Store();
store.runSaga(Sagas);
function Root() {
return (
<Provider store={store}>
<IndexPage />
</Provider>
);
}
export default Root;
这个时候 IndexPage
组件,只需要 action
执行 FETCH_GLOBE_BANN
即可
import React, { useEffect } from 'react';
import { connect } from 'react-redux';
import { FETCH_GLOBE_BANN } from './action';
import './index.css';
function IndexPage(props) {
const { dispatch, testdata, bannlist } = props;
console.log(bannlist); // 输出:(4) [{…}, {…}, {…}, {…}]
const handPull = () => {
dispatch({ type: FETCH_GLOBE_BANN, payload: { username: 'admin', password: '123456' } });
};
return (<button onClick={handPull}>点击</button>);
}
export default connect(({ globe }) => ({
bannlist: globe.bannlist,
testdata: globe.testdata,
}))(IndexPage);
redux-saga 辅助函数
redux-saga
提供了一些辅助函数,包装了一些内部方法,用来在一些特定的 action
被发起到 Store
时派生任务
1):takeEvery(pattern, saga, ...args)
在发起(dispatch)到 Store
并且匹配 pattern
的每一个 action
上派生一个 saga
import { takeEvery, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
yield takeEvery(FETCH_GLOBE_BANN, function* (action) {
const { payload, callback } = action;
console.log(payload); // {username: 'admin', password: '123456'}
});
}
export default [
fork(fetchGlobeBann),
];
2):takeLatest(pattern, saga, ...args)
在发起到 Store
并且匹配 pattern
的每一个 action
上派生一个 saga
,并自动取消之前所有已经启动但仍在执行中的 saga
任务
import { takeLatest, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
yield takeLatest(FETCH_GLOBE_BANN, function* (action) {
const { payload, callback } = action;
console.log(payload); // {username: 'admin', password: '123456'}
});
}
export default [
fork(fetchGlobeBann),
];
3):takeLeading(pattern, saga, ...args)
在发起到 Store
并且匹配 pattern
的每一个 action
上派生一个 saga
, 它将在派生一次任务之后阻塞,直到派生的 saga
完成,然后又再次开始监听指定的 pattern
import { takeLeading, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
yield takeLeading(FETCH_GLOBE_BANN, function* (action) {
const { payload, callback } = action;
console.log(payload); // {username: 'admin', password: '123456'}
});
}
export default [
fork(fetchGlobeBann),
];
4):throttle(ms, pattern, saga, ...args)
在发起到 Store
并且匹配 pattern
的一个 action
上派生一个 saga
,在 ms
毫秒内将暂停派生新的任务,通常用于函数防抖和节流操作
import { throttle, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
// 规定 3秒内重复执行当前 action 会默认阻止后续重复的 action
yield throttle(3000, FETCH_GLOBE_BANN, function* (action) {
const { payload, callback } = action;
console.log(payload); // {username: 'admin', password: '123456'}
});
}
export default [
fork(fetchGlobeBann),
];
5):take(pattern)
创建一个 Effect
描述信息,用来命令 middleware
在 Store
上等待指定的 action
import { take, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
// 无论点击多少次,只要被监听到了就一直阻塞此处
const { payload, callback } = yield take(FETCH_GLOBE_BANN);
console.log(payload);
}
export default [
fork(fetchGlobeBann),
];
需要配合 while(true)
死循环来释放 action
阻塞即可,这样点击多少次就执行多少次
import { take, fork } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
function* fetchGlobeBann() {
while (true) {
const { payload, callback } = yield take(FETCH_GLOBE_BANN);
console.log(payload);
}
}
export default [
fork(fetchGlobeBann),
];
需要注意的是,对比 takeEvery
来说,take
会异步返回 action
参数,可以用于网络请求参数传递
5):call(fn, ...args)
创建一个 Effect
描述信息,用来命令 middleware
以参数 args
调用函数 fn
import { take, fork, call } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN } from './action';
import { getBannData } from './server';
function* fetchGlobeBann() {
while (true) {
const { payload, callback } = yield take(FETCH_GLOBE_BANN);
console.log(payload);
// 异步调用后端接口,并传参数,这个也是 take 的独有之处
const response = yield call(getBannData, payload);
console.log(response);
}
}
export default [
fork(fetchGlobeBann),
];
6):put(action)
创建一个 Effect
描述信息,用来命令 middleware
向 Store
发起一个 action
import { take, fork, call, put } from 'redux-saga/effects';
import { FETCH_GLOBE_BANN, PUTCH_GLOBE_DATA } from './action';
import { getBannData } from './server';
function* fetchGlobeBann() {
while (true) {
const { payload, callback } = yield take(FETCH_GLOBE_BANN);
console.log(payload);
// 异步调用后端接口,并传参数,这个也是 take 的独有之处
const response = yield call(getBannData, payload);
console.log(response);
yield put({ type: PUTCH_GLOBE_DATA, payload: { bannlist: response.banner } });
}
}
export default [
fork(fetchGlobeBann),
];
7):fork(fn, ...args)
创建一个 Effect
描述信息,用来命令 middleware
以非阻塞调用的形式执行 fn
8):race([...effects])
创建一个 Effect
描述信息,用来命令 middleware
在多个 Effect
间运行 竞赛(Race)
9):all([...effects])
创建一个 Effect
描述信息,用来命令 middleware
并行地运行多个 Effect
,并等待它们全部完成
以上只是 redux-saga
的部分 Effect
创建器,更多参考:https://redux-saga-in-chinese.js.org/
推荐使用 take
这种 Effect
,函数防抖和节流操作可以放在交互处进行处理,例:按钮点击禁用,提交增加一个全局 loading
等