1. redux-saga 官方文档地址
// redux-thunk 和 redux-saga
redux-thunk的action要返回函数,这本身已经破坏了react的action的初衷(是个对象)
redux-saga的action还是对象,符合react的要求
redux-saga 对于副作用(数据获取,浏览器缓存获取),易于管理,执行,测试和失败处理,对复杂逻辑能够很好的支持,但是redux-thunk就不能完美支持
// store/index.js (redux的注册地方,使用了中间件redux-saga)
import { createStore, applyMiddleware, combineReducers } from "redux";
import createSaga from "redux-saga";
import logger from "redux-logger";
import { countReducer } from "./counter";
import { userReducer } from "./user";
import rootSaga from './sagas' //saga异步的业务js(根级)
const saga = createSaga();
const store = createStore(
combineReducers({ count: countReducer, user: userReducer }),
applyMiddleware(logger, saga)
);
saga.run(rootSaga); //执行监听
export default store;
// store/sagas.js
// saga具体的api参考官方
// saga的action形式跟redux一样,当redux触发action时候,saga会进行拦截处理,执行监听对应的异步处理,然后通过put继续派发
import { call, put, all, takeEvery } from "redux-saga/effects";
import { toLogin } from "../api"; //这是模拟接口的api
import { REQUEST_LOGIN, LOGINSUCCESS, LOGINFAIL, TOLOGIN } from "./types";
// worker saga 异步的执行函数
function* login(action) {
try {
yield put({ type: REQUEST_LOGIN }); //请求登录
const result = yield call(toLogin, action.name); //接口登录
yield put({ type: LOGINSUCCESS, result }); //登录成功
} catch (message) {
yield put({ type: LOGINFAIL, message }); //登录失败
}
}
// 监听action(TOLOGIN),一旦触发这个action,就执行login
function* watchLogin() {
yield takeEvery(TOLOGIN, login);
}
// 多个监听时候,进行合并监听
export default function* rootSaga() {
yield all([watchLogin()]);
}
// api.js 模拟登录,成功返回和数据
export const toLogin = name => {
return new Promise((resolve,reject) => {
setTimeout(() => {
resolve({ name });
// reject('用户名输入不正确')
}, 2000);
});
};
// store/user.js
// 相比较redux-thunk,action(login)有多改变,返回的不是一个函数,而是个对象
import { REQUEST_LOGIN, LOGINSUCCESS, TOLOGIN, LOGINFAIL } from "./types";
export const userReducer = (state = { isLogin: false, loading: false, error:'' }, action) => {
switch (action.type) {
// 登录中
case REQUEST_LOGIN:
return {
isLogin: false,
loading: true,
error: ''
};
// 登录成功
case LOGINSUCCESS:
return {
isLogin: true,
loading: false,
error: '',
data: action.result
};
// 登录失败
case LOGINFAIL:
return {
isLogin: false,
loading: false,
error: action.message
};
default:
return state;
}
};
// action
// export const login=()=>(dispatch)=>{
// dispatch({type: REQUEST_LOGIN});
// // 异步登录操作模拟
// setTimeout(() => {
// dispatch({type: LOGIN});
// }, 2000);
// }
export const login=(name)=>({type: TOLOGIN,name})
// Login.js 使用的组件
import React, { Component } from "react";
import { connect } from "react-redux";
import { login } from "./store/user";
import { Redirect } from "react-router-dom";
// 包含在空间外层的,form的直接元素,控制布局和样式之类的,错误消息提示
function FormItem(props) {
const { children, status, help } = props;
return (
<div>
{children}
{status === "error" && (
<p style={{ color: "red", fontSize: "12px" }}>{help}</p>
)}
</div>
);
}
// 高阶组件 扩展传入组件功能,返回新的组件
// 增加字段的验证,所有字段的全局验证,表单组件的装饰器[值和name由父级控制]
function MyFormCreate(Com) {
return class extends Component {
constructor(props) {
super(props);
this.state = {};
this.options = {};
}
// 输入框的change事件
handleChange = e => {
const { value, name } = e.target;
this.setState(
{
[name]: value
},
() => {
this.validateField(name); //用回调方式,确保值变了,进行验证
}
);
};
// 全局验证,扩展出去,给使用组件调用
validateAllField = cb => {
const result = Object.keys(this.options).map(name => {
return this.validateField(name);
});
cb(result.every(bool => !!bool), this.state);
};
// 单个字段验证
validateField = name => {
const rules = this.options[name].rules;
const isValid = !rules.some(rule => {
if (rule.required) {
if (!this.state[name]) {
this.setState({
[name + "Message"]: rule.message
});
return true;
}
}
return false;
});
if (isValid) {
this.setState({
[name + "Message"]: ""
});
}
return isValid;
};
// 获取错误信息 给FormItem使用
getErrorMsg = name => {
return this.state[name + "Message"];
};
// 包装表单控件,value和name由父级控制
getFieldDecorator = (name, option, Com) => {
this.options[name] = option;
return (
<div>
{React.cloneElement(Com, {
name: name,
value: this.state[name] || "",
onChange: this.handleChange
})}
</div>
);
};
render() {
return (
<Com
{...this.props}
getFieldDecorator={this.getFieldDecorator}
validateAllField={this.validateAllField}
getErrorMsg={this.getErrorMsg}
/>
);
}
};
}
// 装饰器组件 高阶组件的包装
// 使用组件
@connect(
state => ({ ...state.user }), //这里集体获取用户数据
{ login }
)
@MyFormCreate
class MyForm extends Component {
// 提交
submitForm = () => {
this.props.validateAllField((isValid, data) => {
if (isValid) {
console.log("登录:", data);
console.log(this.props)
this.props.login(data.userName); // 这里登录派发action,传参userName
} else {
console.log("not valid");
}
});
};
render() {
const { getFieldDecorator, getErrorMsg, isLogin, location, loading, error, data } = this.props;
if (isLogin) {
return <Redirect to={location.state.redirect || "/"} />;
}
return (
<div>
{/*这里展示数据和错误*/}
{error && <p style={{color:'red',fontSize:'12px'}}>登录错误:{error}</p>}
{data && <p>登录的姓名: {data.name}</p>}
<FormItem
status={getErrorMsg("userName") ? "error" : ""}
help={getErrorMsg("userName")}
>
{getFieldDecorator(
"userName",
{
rules: [{ required: true, message: "用户名不能为空" }]
},
<input type="text" />
)}
</FormItem>
<FormItem
status={getErrorMsg("password") ? "error" : ""}
help={getErrorMsg("password")}
>
{getFieldDecorator(
"password",
{
rules: [{ required: true, message: "密码不能为空" }]
},
<input type="password" />
)}
</FormItem>
<button onClick={this.submitForm} disabled={loading}>{loading?'登录中...':'登录'}</button>
</div>
);
}
}
export default MyForm;
// 下面这个基本看不到,直接就跳转到页面了
2. 前端React开箱即用框架 UmiJS (内部涵盖了React router和 Dva[基于redux和redux-saga的数据流解决方案]等)
目录:
// config/config.js
export default {
plugins: [
[
"umi-plugin-react",
{
dva: true, //启用dva
antd: true //启用antd,要安装antd
}
]
],
routes: [
{
path: "/login",
component: "./login/index" //根目录是pages下
},
{
path: "/about",
component: "./about/index",
Routes: ["./routes/PrivateRoute.js"] //这个根目录是最外层,和src,config同级
},
{
path: "/",
component: "./index"
},
{
path: "/users",
component: "./users/_layout", //嵌套路由
Routes: ["./routes/PrivateRoute.js"],
routes: [
{
path: "/users",
component: "./users/index"
},
{
path: "/users/:id",
component: "./users/$id"
},
{
component: "./404" //嵌套的路由,404要重新定义,要不然就会用默认的404
}
]
},
{
component: "./404" // 自定义的404,根目录是pages下
}
]
};
// mock数据
// mock/user.js
export default {
"post /api/login": function(req, res) {
setTimeout(() => {
res.json({ login: true });
}, 2000);
}
};
// src/pages/about/index.js
import styles from './index.css';
export default function() {
return (
<div className={styles.normal}>
<h1>这是个About的页面</h1>
</div>
);
}
// src/pages/index.js
import styles from "./index.css";
import Link from "umi/link"; //声明式
import router from "umi/router"; //命令式
export default function() {
return (
<div className={styles.normal}>
<ul>
<li>
<Link to={{ pathname: "/users" }}>用户信息总页</Link>
</li>
<li>
<Link to={{ pathname: "/about" }}>About</Link>
</li>
<li
style={{
textDecoration: "underline",
color: "blue",
cursor: "pointer"
}}
onClick={() => router.push({ pathname: "/users/zs" })}
>
用户信息详情页,带参数
</li>
</ul>
</div>
);
}
// src/pages/login/index.js
import styles from "./index.css";
import { Button } from "antd";
import { connect } from "dva"; // 获取react-redux的connect,这里封装在dva
import Redirect from "umi/redirect"; //跳转
import React, { Component } from "react";
//将redux数据转化成属性
@connect(
state => ({
isLogin: state.user.isLogin, //是否登录
error: state.user.error, //错误
loading: state.loading //这里用的是dva自带的loading
}),
{ login: () => ({ type: "user/login" }) } //登录的action
)
export class index extends Component {
render() {
const { login, isLogin, location, error, loading } = this.props;
{/*登录了就直接跳转,跳转的路由从权限路由高阶组件那边传过来*/}
if (isLogin) return <Redirect to={location.state.redirect} />;
return (
<div className={styles.normal}>
<h1>登录页</h1>
{error && <p style={{ color: "red" }}>错误信息: {error}</p>}
<p>
{/*loading会带有命名空间*/}
<Button disabled={loading.models.user} onClick={login}>
{loading.models.user ? "登录中..." : "登录"}
</Button>
</p>
</div>
);
}
}
export default index;
// src/pages/404.js
import React, { Component } from 'react'
export class NotFound extends Component {
render() {
return (
<div>
页面找不到了,可惜
</div>
)
}
}
export default NotFound
// src/pages/users/_layout.js
import styles from './_layout.css';
export default function(props) {
return (
<div className={styles.normal}>
<h3>这是个嵌套路由,使用约定的_layout,要加上props.children才会有子路由 </h3>
{props.children}
</div>
);
}
// src/pages/users/$id.js
import React, { Component } from "react";
export class $id extends Component {
render() {
const { match } = this.props;
return (
<div>当前的路由参数id:{match.params.id}</div>
);
}
}
export default $id;
// src/pages/users/index.js
import styles from './index.css';
export default function() {
return (
<div className={styles.normal}>
<h1>Page index</h1>
</div>
);
}
// src/utils/api.js
import axios from 'axios'
export const toLogin=()=>{
return axios.post('/api/login');
// return Promise.reject("网络异常")
}
// src/models/user.js
import { toLogin } from "../../utils/api";
export default {
namespace: "user",
state: { isLogin: false, loading: false, error: "", data: "" }, //初始数据state
reducers: {
/**
* 请求登录action
* @param {*} state
* @param {*} action
*/
loginRequest(state, action) {
return {
...state,
isLogin: false,
loading: true,
error: ""
};
},
/**
* 登录成功action
* @param {*} state
* @param {*} action
*/
loginSuccess(state, action) {
return {
...state,
isLogin: true,
loading: false,
error: "",
data: action.res
};
},
/**
* 登录失败action
* @param {*} state
* @param {*} action
*/
loginFail(state, action) {
return {
...state,
isLogin: false,
loading: true,
error: action.message
};
}
},
effects: {
/**
* 登录的saga worder
* @param {*} action
* @param {*} param1
*/
*login(action, { call, put }) {
try {
yield put({ type: "loginRequest" });
const res = yield call(toLogin);
yield put({ type: "loginSuccess", res });
} catch (message) {
yield put({ type: "loginFail", message });
}
}
}
};