九:以理论结合实践方式梳理前端 React 框架 ——— 简述中间件

redux-saga 基本简介

中间件是一种独立运行于各个框架之间的代码,以函数的形式存在,连接在一起,形成一个异步队列,可以访问请求对象和响应对象,可以对请求进行拦截处理,再将处理后的控制权向下传递,终止请求,向客户端做出响应机制,来完成对任何数据的预处理和后处理

中间件的优点在于其灵活性,使用中间件开发者可以用极少的操作就能得到一个插件,用最简单的方法就能够将新的过滤器和处理程序扩展到现有的系统上,最基础的组成部分是:中间件管理器


在这里插入图片描述


要实现中间件模式,最重要的实现细节是:

  • 可以通过调用use()函数来注册新的中间件,通常,新的中间件只能被添加到高压包带的末端,但不是严格要求这么做
  • 当接收到需要处理的新数据时,注册的中间件在意不执行流程中被依次调用。每个中间件都接受上一个中间件的执行结果作为输入值
  • 每个中间件都可以停止数据的进一步处理,只需要简单地不调用它的毁掉函数或者将错误传递给回调函数。当发生错误时,通常会触发执行另一个专门处理错误的中间件

至于怎么处理传递数据,目前没有严格的规则,一般有几种方式:

  • 通过添加属性和方法来增强
  • 使用某种处理的结果来替换 data
  • 保证原始要处理的数据不变,永远返回新的副本作为处理的结果

redux-saga 是一个用于管理应用程序 Side Effect(异步操作) 的 library,它的目标是让副作用管理更容易,执行更高效,测试更简单,在处理故障时更容易,redux-saga 是一个 redux 中间件,通过 action 从主程序启动,暂停和取消,访问完整的 statedispatch action

redux-saga 使用了 ES6 的 Generator 功能,让异步的流程更易于读取,写入和测试,通过这种方式,让异步看起来更加像标准同步

redux-saga 实现原理

redux-saga 是运行在 action 发送出去请求,达到 reducer 之间的一段代码


action
action
action
reducer
state
dispatch
redux
saga
redux
redux
render

简单来看,只要引用了 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 项目,并安装 reduxreact-reduxredux-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.jsroot.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-sagacreateSagaMiddleware 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.jsserver.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 描述信息,用来命令 middlewareStore 上等待指定的 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 描述信息,用来命令 middlewareStore 发起一个 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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值