redux-saga

redux-saga 通过拦截 action 来执行有副作用的 task,以保持 action 的简洁!

redux-thunk 异步计数器

1
2
npx create-react-app saga
yarn add redux react-redux redux-thunk

src/index.js

1
2
3
4
5
6
7
8
9
10
11
12
import React from "react";
import ReactDOM from "react-dom";
import { Provider } from "react-redux";
import store from "./store";
import App from "./App";

ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.querySelector("#root")
);

src/App.jsx

1
2
3
4
5
6
7
8
9
10
11
import React, { Component } from "react";
import Counter from "./pages/counter";
export default class App extends Component {
render() {
return (
<div>
<Counter />
</div>
);
}
}

src/store/reducers.js

1
2
3
4
5
import { combineReducers } from 'redux';
import { reducer as counterReducer } from '../pages/counter/store';
export default combineReducers({
counter: counterReducer
});

src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import rootReducer from './reducers';
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
// 中间件、createStore、reducer
// const store = composeEnhancers(applyMiddleware(thunk))(createStore)(rootReducer);
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(thunk))
);
export default store;

src/pages/counter/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import React, { Component } from 'react'
import { connect } from "react-redux";
import { incrementAsync } from './store/actionCreators';
class Counter extends Component {
render() {
return (
<div>
<p>
{this.props.counter}
</p>
<button onClick={this.props.incrementAsync}>click</button>
</div>
)
}
}
const mapStateToProps = state => {
return {
counter: state.counter,
};
};

export default connect(mapStateToProps, { incrementAsync })(Counter);

src/pages/counter/store/index.js

1
2
3
4
5
6
7
import reducer from './reducer';
import * as actionCreators from './actionCreators';

export {
reducer,
actionCreators
};

src/pages/counter/store/reducer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {
INCREMENT
} from './actionTypes';

const counter = (state = 1, action = {}) => {
switch (action.type) {
case INCREMENT:
return state + 1;
default:
return state;
}
}

export default counter;

src/pages/counter/store/actionCreators.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { INCREMENT } from './actionTypes';

const increment = () => ({
type: INCREMENT
});

export const incrementAsync = () => {
return dispatch => {
setTimeout(() => {
dispatch(increment());
}, 1000);
}
};

src/pages/counter/store/actionTypes.js

1
export const INCREMENT = 'INCREMENT';

redux-thunk异步计数器代码

redux-saga 基础配置

1
yarn add redux-saga

src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { createStore, compose, applyMiddleware } from 'redux';
// #1 引入
import createSagaMiddleware from 'redux-saga';
import rootReducer from './reducers';
import { helloSaga } from '../sagas';
// #2 创建
const sagaMiddleware = createSagaMiddleware();
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
// #3 应用
composeEnhancers(applyMiddleware(sagaMiddleware))
);
// #4 启用,相当于执行了 helloSaga 这个 generator 函数,原理 co 库
sagaMiddleware.run(helloSaga);
export default store;

src/sagas/index.js

1
2
3
export function* helloSaga() {
yield console.log('Hello Saga!');
}

异步计数器

src/pages/counter/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import React, { Component } from 'react'
import { connect } from "react-redux";
import { incrementAsync } from './store/actionCreators';
class Counter extends Component {
render() {
return (
<div>
<p>
{this.props.counter}
</p>
{/* 这里直接执行的是一个 actionCreator,无需手动进行 dispatch 的操作 */}
{/* 这里派发的 action 能被 saga 监听到,然后做对应的处理 */}
<button onClick={this.props.incrementAsync}>click</button>
</div>
)
}
}
const mapStateToProps = state => {
return {
counter: state.counter,
};
};

export default connect(mapStateToProps, { incrementAsync })(Counter);

src/pages/counter/store/actionCreators.js

1
2
3
4
5
6
7
8
9
import { INCREMENT, INCREMENT_ASYNC } from './actionTypes';

export const increment = () => ({
type: INCREMENT
});

export const incrementAsync = () => ({
type: INCREMENT_ASYNC
});

src/sagas/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { takeEvery, delay, put } from 'redux-saga/effects';
import { increment } from '../pages/counter/store/actionCreators';
import { INCREMENT_ASYNC } from '../pages/counter/store/actionTypes';

export function* helloSaga() {
yield console.log('Hello Saga!');
}

// #2 异步代码
function* incrementAsync() {
yield delay(2000);
yield put(increment());
}

// #3 注意不要忘了在外部 run watchIncrementAsync
export function* watchIncrementAsync() {
// #1 监听 action,触发 incrementAsync 函数
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
}

代码

如何组织 saga

当有多个 Saga 时如何同时运行呢,下面为了演示又增加了 <User/> 组件

src/pages/user/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react'
import { connect } from 'react-redux';
import { fetchUser } from './store/actionCreators';

class User extends Component {
render() {
return (
<div>
<button onClick={this.props.fetchUser}>获取数据</button>
</div>
)
}
}

export default connect(null, { fetchUser })(User);

src/App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
import React, { Component } from "react";
import Counter from "./pages/counter";
import User from "./pages/user";
export default class App extends Component {
render() {
return (
<div>
<Counter />
<User />
</div>
);
}
}

src/pages/user/store/actionCreators.js

1
2
3
4
5
6
import { FETCH_USER } from './actionTypes';
export const fetchUser = () => {
return {
type: FETCH_USER
};
}

src/pages/user/store/actionTypes.js

1
export const FETCH_USER = 'FETCH_USER';

src/sagas/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { takeEvery, delay, put, call } from 'redux-saga/effects';
import axios from 'axios';
import { increment } from '../pages/counter/store/actionCreators';
import { INCREMENT_ASYNC } from '../pages/counter/store/actionTypes';
import { FETCH_USER } from "../pages/user/store/actionTypes";

function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
function* fetchUser() {
const user = yield call(axios.get, "https://api.github.com/users");
console.log(user);
}

export default function* rootSaga() {
// 你会发现 10s 之内点击按钮或请求数据无效,因为这里是串行的
yield delay(10000);
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
yield takeEvery(FETCH_USER, fetchUser);
}

利用 all

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// #1 worker saga
function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
function* fetchUser() {
const user = yield call(axios.get, "https://api.github.com/users");
console.log(user);
}

// #2 watcher saga
function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
}
function* watchFetchUser() {
yield takeEvery(FETCH_USER, fetchUser);
}
// #3 root saga
export default function* rootSaga() {
yield all([
watchIncrementAsync(),
watchFetchUser()
]);
}

拆分不同的文件

src/sagas/counter.js

1
2
3
4
5
6
7
8
9
10
11
import { takeEvery, delay, put } from 'redux-saga/effects';
import { increment } from '../pages/counter/store/actionCreators';
import { INCREMENT_ASYNC } from '../pages/counter/store/actionTypes';

function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
export function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
}

src/sagas/user.js

1
2
3
4
5
6
7
8
9
10
11
import axios from 'axios';
import { takeEvery, call } from 'redux-saga/effects';
import { FETCH_USER } from "../pages/user/store/actionTypes";

function* fetchUser() {
const user = yield call(axios.get, "https://autumnfish.cn/api/joke");
console.log(user);
}
export function* watchFetchUser() {
yield takeEvery(FETCH_USER, fetchUser);
}

src/sagas/index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { all, fork } from "redux-saga/effects";
import * as counterSagas from './counter';
import * as userSagas from './user';

export default function* rootSaga() {
// #1 需要自己执行每一个 watch saga
/* yield all([
counterSagas.watchIncrementAsync(),
userSagas.watchFetchUser(),
]); */
// #2 可以利用 fork 帮我们执行 watch saga
/* yield all([
fork(counterSagas.watchIncrementAsync),
fork(userSagas.watchFetchUser),
]); */
// #3 优化优化
yield all([
...Object.values(userSagas),
...Object.values(counterSagas)
].map(fork));
}

每个文件组织好后再导出

src/sagas/counter.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import { takeEvery, delay, put } from 'redux-saga/effects';
import { increment } from '../pages/counter/store/actionCreators';
import { INCREMENT_ASYNC } from '../pages/counter/store/actionTypes';

function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
function* watchIncrementAsync() {
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
}

export const counterSagas = [
watchIncrementAsync();
];

src/sagas/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
import { takeEvery, call } from 'redux-saga/effects';
import axios from 'axios';
import { FETCH_USER } from "../pages/user/store/actionTypes";
function* fetchUser() {
const user = yield call(axios.get, "https://autumnfish.cn/api/joke");
console.log(user);
}
function* watchFetchUser() {
yield takeEvery(FETCH_USER, fetchUser);
}
export const userSagas = [
watchFetchUser();
]

src/sagas/index.js

1
2
3
4
5
6
7
8
9
10
import { all } from "redux-saga/effects";
import { counterSagas } from './counter';
import { userSagas } from './user';

export default function* rootSaga() {
yield all([
...counterSagas,
...userSagas
]);
}

单元测试

1
yarn add @babel/core @babel/node @babel/plugin-transform-modules-commonjs tape
1
2
3
4
5
{
"scripts": {
"test": "babel-node src/sagas/counter.test.js --plugins @babel/plugin-transform-modules-commonjs",
},
}

src/sagas/counter.js

1
2
3
4
5
// 这里需要导出,用于单元测试
export function* incrementAsync() {
yield delay(2000);
yield put(increment());
}

src/sagas/sagas/counter.test.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import test from 'tape';
import { delay, put } from 'redux-saga/effects';
import { incrementAsync } from './counter';
import { increment } from '../pages/counter/store/actionCreators';

test('incrementAsync saga test', function(assert) {
const it = incrementAsync();
// yield 什么,这里的 it.next().value 就是什么
assert.deepEqual(
it.next().value,
delay(2000),
"A promise with a delay of 2 s should be returned"
);
assert.deepEqual(
it.next().value,
put(increment()),
"An increase action should be initiated"
);
assert.end();
});
1
npm test

take 和 select

take

1
2
3
4
5
6
7
8
9
10
11
export function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
function* watchIncrementAsync() {
// yield takeEvery(INCREMENT_ASYNC, incrementAsync);
// 只会箭头触发的第一次 action
const action = yield take(INCREMENT_ASYNC);
console.log(action); // {type: "INCREMENT_ASYNC"}
yield incrementAsync();
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 触发 2 次
export function* incrementAsync() {
yield delay(2000);
yield put(increment());
}
function* watchIncrementAsync() {
// yield take(INCREMENT_ASYNC);
// yield incrementAsync();
// yield take(INCREMENT_ASYNC);
// yield incrementAsync();
for(let i = 0; i < 2; i ++) {
yield take(INCREMENT_ASYNC);
yield incrementAsync();
}
console.log('会执行 2 次');
}
1
2
3
4
5
6
7
// 模拟 takeEvery
function* watchIncrementAsync() {
while(true) {
yield take(INCREMENT_ASYNC);
yield incrementAsync();
}
}

takeEvery

1
2
3
4
export function* watchIncrementAsync() {
// takeEvery 会监听每一次 action
yield takeEvery(INCREMENT_ASYNC, incrementAsync);
}

takeLatest

1
2
3
4
export function* watchIncrementAsync() {
// takeLatest 会以最后一次 action 为准
yield takeLatest(INCREMENT_ASYNC, incrementAsync);
}

select

1
2
3
4
5
6
7
8
9
function* watchAll() {
while(true) {
// 监听所有的 action,获取最新的状态树
console.log(yield take('*'));
console.log(yield select());
// 也可以传递函数对状态进行过滤
// console.log(yield select(state => state.counter));
}
}

call/apply 和 cps

都可以调用方法并传递参数,以及改变方法中的 this 指向

1
2
3
4
5
6
7
8
export function* incrementAsync() {
// delay 函数中的 this 就是 o,注意 delay 不能是一个箭头函数
const o = { name: 'ifer' };
yield call([o, delay], 2000);
// 同样 apply 也可以调用函数,参数分别是this、函数、参数
// yield apply(o, delay, [2000]);
yield put(increment());
}
1
2
3
4
5
6
7
// delay 是自己封装的,不是 saga 提供的,注意用到 this 的话这里的 delay 不能是一个箭头函数
const delay = function (ms) {
return new Promise(resolve => {
console.log(this);
setTimeout(resolve, ms);
});
}

cps

1
2
3
4
5
6
7
8
9
10
// Node style function的方式调用 fn
const getCon = function(type, callback) {
setTimeout(() => {
callback(null, type + ' hello world');
}, 1000);
};

// call 只能用于调用返回 Promise 的方法,cps 可以等待回调的返回结果
let con = yield cps(getCon, 'xxx');
console.log(con)

all 和 race

all

1
2
3
4
5
6
7
// 可以并行执行任务
function* incrementAsync() {
yield all([
delay(2000),
put(increment())
]);
}

一般用于组织多个 Saga,并行执行,作为 rootSaga 统一导出

1
2
3
4
5
6
export default function* rootSaga() {
yield all([
watchIncrementAsync(),
watchFetchUser()
]);
}
1
2
3
4
5
6
7
8
// 也可以这样使用
function* fetchJoke() {
const [data1, data2] = yield all([
call(axios.get, "https://autumnfish.cn/api/joke"),
call(axios.get, "https://autumnfish.cn/api/joke/list?num=3")
]);
console.log(data1, data2);
}

race

1
2
3
4
5
6
7
8
// 期望同时启动多个任务,只希望拿到胜利者:第一个被 resolve(或 reject)的任务
function* raceTask() {
const {a, b} = yield race({
a: delay(1000),
b: delay(500)
});
console.log(a, b); // undefined true
}

loading 和错误处理

src/pages/user/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
import React, { Component } from 'react'
import { connect } from 'react-redux';
import { fetchUser } from './store/actionCreators';

class User extends Component {
render() {
const { isFetching, error, user } = this.props.user;
let data = null;
if(error) {
data = error;
} else if(isFetching) {
data = "Loading...";
} else {
data = user && <ul>{
user.jokes.map((item,index) => <li key={index}>{item}</li>)
}</ul>;
}
return (
<div>
<div>{data}</div>
<button onClick={this.props.fetchUser}>获取数据</button>
</div>
)
}
}

const mapStateToProps = state => {
return {
user: state.user
};
};

export default connect(mapStateToProps, { fetchUser })(User);

src/pages/user/store/index.js

1
2
import reducer from './reducer';
export { reducer };

src/pages/user/store/reducer.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
import { FETCH_USER, FETCH_USER_SUCCESS, FETCH_USER_FAILURE } from './actionTypes';
const initialState = {
isFetching: false,
error: null,
user: null
};
export default function(state=initialState, action) {
switch(action.type) {
case FETCH_USER:
return {
isFetching: true,
error: null,
user: null
};
case FETCH_USER_SUCCESS:
return {
isFetching: false,
error: null,
user: action.payload
};
case FETCH_USER_FAILURE:
return {
isFetching: false,
error: action.error,
user: null
};
default:
return state;
}
}

src/pages/user/store/actionTypes.js

1
2
3
export const FETCH_USER = 'FETCH_USER';
export const FETCH_USER_SUCCESS = 'FETCH_USER_SUCCESS';
export const FETCH_USER_FAILURE = 'FETCH_USER_FAILURE';

src/sagas/user.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { takeEvery, call, put } from 'redux-saga/effects';
import axios from 'axios';
import { FETCH_USER, FETCH_USER_SUCCESS, FETCH_USER_FAILURE } from "../pages/user/store/actionTypes";
// #1 失败了,后台返回对应的 HTTP Code,前端针对状态码处理
// #2 无论成功或失败,后端 HTTP Code 都响应 200,然后提供不同的 status 标识符,前端据此进行判断
function* fetchUser() {
try {
const { data } = yield call(axios.get, "https://autumnfish.cn/api/joke/list?num=3");
yield put({ type: FETCH_USER_SUCCESS, payload: data });
} catch(e) {
yield put({ type: FETCH_USER_FAILURE, error: e.message });
}
}
function* watchFetchUser() {
yield takeEvery(FETCH_USER, fetchUser);
}
export const userSagas = [
watchFetchUser()
];

src/store/reducers.js

1
2
3
4
5
6
7
import { combineReducers } from 'redux';
import { reducer as counterReducer } from '../pages/counter/store';
import { reducer as userReducer } from '../pages/user/store';
export default combineReducers({
counter: counterReducer,
user: userReducer
});

代码

登录流程

redux-saga 配置和登录界面

1
npx create-react-app saga-login
1
yarn add redux react-redux redux-saga

src/index.js

1
2
3
4
5
6
7
import React from 'react';
import ReactDOM from 'react-dom';
import { Provider } from 'react-redux';
import App from './App';
import store from './store';

ReactDOM.render(<Provider store={store}><App/></Provider>, document.querySelector('#root'));

src/App.jsx

1
2
3
4
5
6
7
8
9
10
11
12
import React, { Component } from 'react'
import Login from './pages/login';

export default class App extends Component {
render() {
return (
<div>
<Login/>
</div>
)
}
}

src/store/index.js

1
2
3
4
5
6
7
8
9
10
11
12
import { createStore, compose, applyMiddleware } from 'redux';
import createSagaMiddleware from 'redux-saga'; // #1
import rootReducer from './reducers';
import rootSaga from '../sagas';
const sagaMiddleware = createSagaMiddleware(); // #2
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
rootReducer,
composeEnhancers(applyMiddleware(sagaMiddleware)) // #3
);
sagaMiddleware.run(rootSaga); // #4
export default store;

src/store/reducers/index.js

1
2
3
4
5
6
import { combineReducers } from 'redux';
import loginReducer from './login';

export default combineReducers({
login: loginReducer
});

src/store/reducers/login.js

1
2
3
4
const initState = {};
export default function(state=initState, action) {
return state;
}

src/sagas/index.js

1
2
3
4
5
6
7
8
import { all } from "redux-saga/effects";
import loginSagas from './login';

export default function* rootSaga() {
yield all([
...loginSagas,
]);
}

src/sagas/login.js

1
2
3
4
5
6
7
export function* login() {
yield console.log('hello saga');
}

export default [
login()
];

src/pages/login/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
import React, { Component } from 'react'
import { connect } from 'react-redux';

class Login extends Component {
renderLogin = () => {
return <>
<input type="text" placeholder="输入用户名"/>
<input type="text" placeholder="输入密码"/>
<button>登录</button>
</>;
}
renderLogout = () => {
return <button>退出</button>;
}
render() {
const { token } = this.props.loginData;
return (
<div>
{
token
?
this.renderLogout()
:
this.renderLogin()
}
</div>
)
}
}

const mapStateToProps = state => ({
loginData: state.login
});
export default connect(mapStateToProps, null)(Login);

登录功能

src/pages/login/index.jsx

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
import React, { Component } from 'react'
import { connect } from 'react-redux';
import * as loginAction from '../../store/action/login';

class Login extends Component {
state = {
userInfo: {
username: "",
password: ""
}
}
handleChange = e => {
this.setState({
userInfo: {
...this.state.userInfo,
[e.target.name]: e.target.value
}
});
}
renderLogin = () => {
const { username, password } = this.state.userInfo;
return <>
<input type="text" name="username" value={username} placeholder="输入用户名" onChange={this.handleChange}/>
<input type="text" name="password" value={password} placeholder="输入密码" onChange={this.handleChange}/>
<button onClick={() => this.props.login(this.state.userInfo)}>登录</button>
</>;
}
renderLogout = () => {
return <button onClick={this.props.logout}>退出</button>;
}
render() {
const { token, isFetching, error } = this.props.loginData;
let data = null;
if(error) {
data = error;
} else if(isFetching) {
data = 'loading...';
} else {
data = token;
}
return (
<div>
<p>{data}</p>
{
token
?
this.renderLogout()
:
this.renderLogin()
}
</div>
)
}
}

const mapStateToProps = state => ({
loginData: state.login
});
export default connect(mapStateToProps, loginAction)(Login);

src/sagas/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import { takeEvery, call, put } from 'redux-saga/effects';
import * as loginAT from '../store/constant/login';
import API from '../utils/api';

// #2 worker saga
function* login(action) {
try {
const { username, password } = action.payload;
const res = yield call(API.login, username, password);
yield put({ type: loginAT.LOGIN_SUCCESS, payload: res});
} catch(error) {
yield put({ type: loginAT.LOGIN_FAILED, error});
}
}

// #1 watcher saga
function* watchLogin() {
yield takeEvery(loginAT.LOGIN, login);
}

export default [
watchLogin()
];

src/store/action/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import * as loginAT from '../constant/login';

export const login = payload => {
return {
type: loginAT.LOGIN,
payload
};
};

export const logout = () => {
return {
type: loginAT.LOGOUT
}
};

src/store/constant/login.js

1
2
3
4
export const LOGIN = 'LOGIN';
export const LOGOUT = 'LOGOUT';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILED = 'LOGIN_FAILED';

src/store/reducers/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
import * as loginAT from '../constant/login';
const initState = {
// isFetching: false,
// error: null,
// token: null
};
export default function(state=initState, action) {
switch(action.type) {
case loginAT.LOGIN:
return {
isFetching: true,
};
case loginAT.LOGIN_SUCCESS:
return {
token: action.payload,
isFetching: false
};
case loginAT.LOGIN_FAILED:
return {
error: action.error,
isFetching: false
};
default:
return state;
}
}

src/utils/api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
const login = (username, password) => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (username === 'ifer' && password === '123') {
resolve('login success');
} else {
reject('login error');
}
}, 1000);
});
}

export default { login };

退出功能

src/sagas/login.js

1
2
3
4
5
6
7
8
9
10
function *logout() {
yield put({ type: loginAT.LOGOUT_SUCCESS });
}
function* watchLogout() {
yield takeEvery(loginAT.LOGOUT, logout);
}
export default [
watchLogin(),
watchLogout()
];

src/store/constant/login.js

1
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';

src/store/reducers/login.js

1
2
3
4
5
6
7
8
export default function(state=initState, action) {
switch(action.type) {
case loginAT.LOGOUT_SUCCESS:
return {};
default:
return state;
}
}

如何取消登录

src/pages/login/index.jsx

1
<button onClick={this.props.cancelLogin}>取消登录</button>

src/sagas/login.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
import { takeEvery, call, put, cancel, fork } from 'redux-saga/effects';
import * as loginAT from '../store/constant/login';
import API from '../utils/api';

// #2 worker saga
function* login(action) {
try {
const { username, password } = action.payload;
const res = yield call(API.login, username, password);
yield put({ type: loginAT.LOGIN_SUCCESS, payload: res});
} catch(error) {
yield put({ type: loginAT.LOGIN_FAILED, error});
} finally {
// 解决取消登录时一直显示 loading...
yield put({ type: loginAT.FETCH_DONE });
}
}

// #1 watcher saga
function* watchLogin() {
yield takeEvery(loginAT.LOGIN, function*(action) {
// fork 也能执行 login,返回一个任务对象
const task = yield fork(login, action);
yield takeEvery(loginAT.LOGIN_CANCEL, function*() {
// LOGIN_CANCEL 时 reducer 不需要执行退出操作,这里只是取消登录
yield cancel(task);
});
});
}


function *logout() {
yield put({ type: loginAT.LOGOUT_SUCCESS });
}
function* watchLogout() {
yield takeEvery(loginAT.LOGOUT, logout);
}

export default [
watchLogin(),
watchLogout()
];

src/store/action/login.js

1
2
3
export const cancelLogin = () => ({
type: loginAT.LOGIN_CANCEL
});

src/store/constant/login.js

1
2
3
4
5
6
7
8
9
export const LOGIN = 'LOGIN';
export const LOGIN_SUCCESS = 'LOGIN_SUCCESS';
export const LOGIN_FAILED = 'LOGIN_FAILED';
export const LOGIN_CANCEL = 'LOGIN_CANCEL';

export const LOGOUT = 'LOGOUT';
export const LOGOUT_SUCCESS = 'LOGOUT_SUCCESS';

export const FETCH_DONE = 'FETCH_DONE';

src/store/reducers/login.js

1
2
3
4
5
6
7
8
9
10
export default function(state=initState, action) {
switch(action.type) {
case loginAT.FETCH_DONE:
// 把之前登录成功的数据收集下
return {
...state,
isFetching: false
};
}
}