react-redux+redux-saga优化

·  阅读 699

最简组件

Simplest
├─ index.js
├─ index.less
├─ reducer.js
└─ saga.js复制代码

一个React最简组件,应该包括四个部分:
  • 主js
  • 样式文件
  • 组件reducer文件
  • 组件saga文件

其中saga文件在不同的情况下,如该组件页面为静态页面,无需与后台接口打交道,那么组件saga文件可以省略。

index.js

import React, {Component} from "react";
import "./index.less";
import {connect} from "react-redux";
import {withRouter} from "react-router-dom";
import {injectIntl, FormattedMessage} from 'react-intl';

class Comp extends Component {
  constructor() {
    super();
    this.state = {};
  }

  componentDidMount() {

  }

  componentWillReceiveProps(nextProps) {
  }

  //国际化key为纯字符串
  renderFormattedString = (key) => {
    const {intl} = this.props;
    let res = intl.formatMessage({id: key, defaultMessage: key});
    // console.log(res);
    return res;
  };

  //国际化key为react node,一般为span标签
  renderFormattedMessage = (key) => {
    return <FormattedMessage
      id={key}
      defaultMessage={key}
    />
  };

  render() {
    return (
      <div>
      </div>
    );
  }
}

//映射state到容器组件props
const mapStateToProps = state => ({
  xxx_result: state.xxx_result
});

//映射dispatch到容器组件props
const mapDispatchToProps = dispatch => ({
  xxx: param => {
    dispatch({
      type: 'xxx',
      payload: param
    });
  }
});

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(withRouter(injectIntl(Comp)));复制代码

saga.js

let saga = {};
//登录
saga["xxx"] = {
  type: "post",
  path: "/api/xxx"
};

export default saga;复制代码

reducer.js

const reducer = {
  xxx_result: (state = {success: "", data: [], msg: ""},
               action) => {
    const keyword = "xxx";
    switch (action.type) {
      case `${keyword}_success`:
        return {
          success: true,
          data: action.payload.data,
          msg: action.payload.msg
        };
      case `${keyword}_fail`:
        return {
          success: false,
          data: [],
          msg: action.payload.msg
        };
      case `init_${keyword}`:
        return {
          success: "", data: [], msg: ""
        };
      default:
        return state;
    }
  }
};

export default reducer;复制代码

redux结构

keyword(redux action keyword),与saga对应action keyword形成对应
对应action keyword返回的redux状态,名称形式为"xxx_result"
与saga异步请求接口状态对应:
  • 接口请求成功,转发形式为"xxx_success"的action,返回redux状态
  • 接口请求失败,转发形式为"xxx_fail"的action,返回redux状态
  • 初始化redux状态,转发形式为"init_xxx"的action

saga汇聚

sagas.js

import {takeEvery, call, put} from 'redux-saga/effects';
import request from '../utils/request';
import {urlEncode} from '@/utils';
import {message} from 'antd';
import Register from '@/views/Register/saga';
import Login from '@/views/Login/saga';
import DefaultPage from "@/containers/DefaultPage/saga";
import AccountCenter from "@/containers/AccountCenter/saga";
import FinancialCenter from "@/containers/FinancialCenter/saga";
import Withdrawals from "@/containers/Withdrawals/saga";
import CampaignPlaza from "@/containers/CampaignPlaza/saga";
import CampaignDetail from "@/containers/CampaignDetail/saga";
import MyCase from "@/containers/MyCase/saga";
import CaseDetail from "@/containers/CaseDetail/saga";
import PaymentInfo from "@/containers/PaymentInfo/saga";
import AddAccount from "@/containers/AddAccount/saga";
import ForgetLoginPassword from "@/containers/ForgetLoginPassword/saga";
import ResetLoginPassword from "@/containers/ResetLoginPassword/saga";
import ResetWithdrawPassword from "@/containers/ResetWithdrawPassword/saga";
import UpdateUserInfo from "@/containers/UpdateUserInfo/saga";
import RegSuccess from "@/containers/RegSuccess/saga";


// 根据action.type执行异步请求(sage)
function executeSaga(action) {
	return function* ({type, path}) {
		const lang = localStorage.getItem('IMS_INF_LANG');
		const lNewsRead = localStorage.getItem("IMS_INF_LNEWS_READ");
		const params = action.payload;
		//定义请求者
		const requester = () => {
			if (type === 'post') {
				return request.post(path, params);
			} else if (type === 'get') {
				return request.get(path + "?" + urlEncode(params));
			}
		};
		try {
			//调用请求者获取数据
			const json = yield call(requester);
			let typeMsg = '';
			switch (json.code) {
				case 0:
					typeMsg = `${action.type}_success`;
					break;
				case 19:
					message.info(lang === "zh" ? "请重新登录!": "please relogin!");
					localStorage.clear();
					localStorage.setItem("IMS_INF_LANG", lang);
					localStorage.setItem("IMS_INF_LNEWS_READ", lNewsRead);
					window.location.href = '/login';
					break;
				default:
					typeMsg = `${action.type}_fail`;
					//如果没有错误信息,则构建错误信息
					if (!json.msg) {
						json.msg = lang === "zh" ? "请求错误!": "request error!";
					}
					// 未激活登录,不提示错误,错误信息在页面上展示
					// 激活页面,错误信息在页面上展示
					if (!((path === "/user/login" && json.code === 30067) || (path === "/user/active"))) {
						message.error(json.msg);
					}
					break;
			}
			yield put({
				type: `${typeMsg}`,
				payload: json
			});
			/*
			异步请求完毕后,改变redux的状态
			页面依赖于对应redux状态的,根据该改变去进行重新渲染
			注意,加上初始化对应redux状态的操作,
			主要是为了保证每次发起异步请求时,对应redux的状态都是初始化的状态
			因为页面有些提示信息需要每次发起异步请求都需要给到用户即时的响应信心
			如果异步请求完毕后,不加初始化对应redux状态的操作
			会造成每次用户操作,只有第一次会给出提示信息的情况,造成用户体验差的不良影响
			* */
			yield put({
				type: `init_${action.type}`,
				payload: json
			});
		} catch (error) {
			console.error(error);
			let infoText = lang === "zh" ? "接口请求异常!": "api request error!";
			// 请求超时!或 服务器地址错误或网络异常,请检查后重试!处理逻辑
			if (error.message) {
				if (error.message === "Network Error") {
					infoText = lang === "zh" ? "网络异常!": "network error!";
				} else if (error.message.indexOf("timeout") !== -1) {
					infoText = lang === "zh" ? "请求超时!": "time out!";
				}
			}
			yield put({
				type: `${action.type}_fail`,
				payload: {
					msg: infoText
				}
			});
			message.error(infoText);
			// 错误捕捉 网络异常 或 请求超时 仍然需要对redux状态进行初始化处理
			yield put({
				type: `init_${action.type}`,
				payload: {}
			});
		}
	};
}

const sagas = Object.assign(
	{},
	Login,
	Register,
	DefaultPage,
	AccountCenter,
	FinancialCenter,
	Withdrawals,
	CampaignPlaza,
	CampaignDetail,
	MyCase,
	CaseDetail,
	PaymentInfo,
	AddAccount,
	ForgetLoginPassword,
	ResetLoginPassword,
	ResetWithdrawPassword,
	UpdateUserInfo,
	RegSuccess
);

function* rootSaga() {
	for (let key in sagas) {
		yield takeEvery(key, action => executeSaga(action)(sagas[key]));
	}
}

export default rootSaga;复制代码

saga汇聚层,每次异步请求改变redux状态之后,需要马上执行action将对应redux状态进行初始化,以保证每次异步请求时redux状态的纯净性。之前,没有把init action提到saga汇聚层,在需要引入redux action之处,每次进行一次action之后都需要手动init,费事耗力,而且不方便维护。提到saga汇聚层之后,redux状态流转脉络更加清晰,不用在每个组件中单独引入初始化action,减少冗余代码。

  componentWillReceiveProps(nextProps) {
    // console.log("componentWillReceiveProps");
    if (nextProps.login_result.success !== this.props.login_result.success) {
      if (nextProps.login_result.success) {
        this.setState({
          submitLoading: false,
          unActivated: false
        });
        message.success(nextProps.login_result.msg);
        localStorage.setItem("IMS_INF_TOKEN", nextProps.login_result.data.sign);
        localStorage.setItem("IMS_INF_NAME", nextProps.login_result.data.last_name);
        this.props.update_user_info();
        this.props.history.push('/home');
      } else if (nextProps.login_result.success === false) {
        if (nextProps.login_result.code === 30067) {
          this.setState({
            unActivated: true,
            sign: nextProps.login_result.data
          });
        } else {
          this.setState({
            unActivated: false
          });
        }
        this.setState({
          submitLoading: false
        });
        // 如果有缓存,则清空缓存
        localStorage.removeItem("IMS_INF_TOKEN");
        localStorage.removeItem("IMS_INF_NAME");
      }
    }
    if (nextProps.resend_active_mail_result.success !== this.props.resend_active_mail_result.success) {
      if (nextProps.resend_active_mail_result.success) {
        message.success(nextProps.resend_active_mail_result.msg);
        // this.toggleTip();
        this.startTimer();
      }
    }
  }复制代码

如果在saga汇聚层没有init action

yield put({
    type: `init_${action.type}`,
    payload: json
});复制代码

则会出现这种现象(以登录接口为例):
第一次点击登录按钮,接口响应登录失败,正常提示异常信息。
第二次点击登录按钮,接口响应登录失败,不出现异常信息提示。
原因分析:
第一次请求登录接口,nextProps.login_result.success = false,this.props.login_result.success = ""
状态发生变化

if (nextProps.login_result.success !== this.props.login_result.success) {
    if (nextProps.login_result.success) {
        ......
    } else if (nextProps.login_result.success === false) {
        ......
        // 此时进入该处,登录按钮loading状态消失
        this.setState({
          submitLoading: false
        });
    }
}复制代码
状态未发生变化,不会进入下面if逻辑

if (nextProps.login_result.success !== this.props.login_result.success) {
    if (nextProps.login_result.success) {
        ......
    } else if (nextProps.login_result.success === false) {
        ......
        // 此时不会进入该处,登录按钮loading状态一直不消失
        this.setState({
          submitLoading: false
        });
    }
}复制代码

如果在saga汇聚层加入init action

第一次请求登录接口,nextProps.login_result.success = false,this.props.login_result.success = ""

状态发生变化

if (nextProps.login_result.success !== this.props.login_result.success) {
    if (nextProps.login_result.success) {
        ......
    } else if (nextProps.login_result.success === false) {
        ......
        // 此时进入该处,登录按钮loading状态消失
        this.setState({
          submitLoading: false
        });
    }
}复制代码
第二次请求登录接口,nextProps.login_result.success = false,this.props.login_result.success = ""
状态发生变化,依然能够进入下面if逻辑

if (nextProps.login_result.success !== this.props.login_result.success) {
    if (nextProps.login_result.success) {
        ......
    } else if (nextProps.login_result.success === false) {
        ......
        // 此时进入该处,登录按钮loading状态消失
        this.setState({
          submitLoading: false
        });
    }
}复制代码

错误异常捕获

try {
	//调用请求者获取数据
	const json = yield call(requester);
	let typeMsg = '';
	switch (json.code) {
		case 0:
			typeMsg = `${action.type}_success`;
			break;
		case 19:
			message.info(lang === "zh" ? "请重新登录!": "please relogin!");
			localStorage.clear();
			localStorage.setItem("IMS_INF_LANG", lang);
			localStorage.setItem("IMS_INF_LNEWS_READ", lNewsRead);
			window.location.href = '/login';
			break;
		default:
			typeMsg = `${action.type}_fail`;
			//如果没有错误信息,则构建错误信息
			if (!json.msg) {
				json.msg = lang === "zh" ? "请求错误!": "request error!";
			}
			// 未激活登录,不提示错误,错误信息在页面上展示
			// 激活页面,错误信息在页面上展示
			if (!((path === "/user/login" && json.code === 30067) || (path === "/user/active"))) {
				message.error(json.msg);
			}
			break;
	}
	yield put({
		type: `${typeMsg}`,
		payload: json
	});
	/*
	异步请求完毕后,改变redux的状态
	页面依赖于对应redux状态的,根据该改变去进行重新渲染
	注意,加上初始化对应redux状态的操作,
	主要是为了保证每次发起异步请求时,对应redux的状态都是初始化的状态
	因为页面有些提示信息需要每次发起异步请求都需要给到用户即时的响应信心
	如果异步请求完毕后,不加初始化对应redux状态的操作
	会造成每次用户操作,只有第一次会给出提示信息的情况,造成用户体验差的不良影响
	* */
	yield put({
		type: `init_${action.type}`,
		payload: json
	});
} catch (error) {
	console.error(error);
	let infoText = lang === "zh" ? "接口请求异常!": "api request error!";
	// 请求超时!或 服务器地址错误或网络异常,请检查后重试!处理逻辑
	if (error.message) {
		if (error.message === "Network Error") {
			infoText = lang === "zh" ? "网络异常!": "network error!";
		} else if (error.message.indexOf("timeout") !== -1) {
			infoText = lang === "zh" ? "请求超时!": "time out!";
		}
	}
	yield put({
		type: `${action.type}_fail`,
		payload: {
			msg: infoText
		}
	});
	message.error(infoText);
	// 错误捕捉 网络异常 或 请求超时 仍然需要对redux状态进行初始化处理
	yield put({
		type: `init_${action.type}`,
		payload: {}
	});
}复制代码

catch到错误异常后,根据error.message进行初步判断错误异常原因,主要分为两种情况:
  • 网络异常:断网或网络异常(error.message === "Network Error")
  • 请求超时(error.message包含"timeout of xx",xx取决于axios设置的timeout时长)


分类:
前端
标签:
分类:
前端
标签:
收藏成功!
已添加到「」, 点击更改