写这篇文章的动力有两个:一个是我在 github 上的 redux-async-demo 获得了出乎意料的star数(虽然不多,但也是对我自己莫大的鼓励);另一方面,在有了一定的项目经验之后,想对整个流程再梳理一遍,方便自己日后进行参考。
一、目录结构:
关于 Dumb 组件(纯组件)和 Smart 组件(业务组件)的划分,请参考 React.js 小书:Smart 组件 vs Dumb 组件。说实话,刚开始学 React 的时候,对这些概念都不是很理解,后来,当我自己完全接手一个大项目,尝试着去运用的时候,才真正明白这种组件划分真的是 amazing!
二、模块间调用关系
(1)入口:app.js
app.jsx
为启动入口,配置好 Thunk/Saga,将 store
传入业务组件 <App />
(/container/index.jsx)
// app.js
import '../asset/css/style.scss';
import 'antd/dist/antd.min.css';
import React from 'react';
import { render } from 'react-dom';
import { Provider } from 'react-redux';
import { createStore, applyMiddleware, combineReducers } from 'redux';
import logger from 'redux-logger';
import thunk from 'redux-thunk';
import createSagaMiddleware from 'redux-saga';
import axios from 'axios';
import appReducer from './redux/reducer';
import App from './container';
import rootSaga from './redux/sagas';
const sagaMiddleware = createSagaMiddleware();
const middlewares = [thunk, sagaMiddleware, logger];
const store = createStore(appReducer, applyMiddleware(...middlewares));
sagaMiddleware.run(rootSaga);
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('app')
);
(2)业务组件 vs 纯组件
<App />
(/container/index.jsx)为 container 中的业务组件入口,专门用来获取异步数据,这样就可以跟 component 中的纯组件解耦;
component 中的组件不用关心外部的情况,只需要关注传入的 props
进行渲染即可。
每个 Smart 组件跟 Dumb 组件都通过
connect
传递props
:通过mapStateToProps
传递 state,通过mapDispatchToProps
传递 dispatch。
如:
import { connect } from 'react-redux';
import axios from 'axios';
import { GET_POSTS_SAGA } from 'constant/actionTypes';
import PostList from 'component/PostList';
const mapStateToProps = (state) => ({
posts: state.posts.posts
});
const mapDispatchToProps = (dispatch) => ({
fetchPosts: () => dispatch({ type: GET_POSTS_SAGA })
});
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
(3)两种异步流处理方式
Thunk 处理流程 ☟
Thunk 直接在业务组件中 dispatch
一个函数来异步获取数据。
/container/UserList.js
import { connect } from 'react-redux';
import axios from 'axios';
import UserList from 'component/UserList';
import {
GET_USERS_SUCESS,
GET_USERS_FAIL
} from 'constant/actionTypes';
import {
GET_USERS_URL
} from 'constant/url';
const mapStateToProps = (state) => ({
users: state.users
});
const mapDispatchToProps = (dispatch) => ({
fetchUsers: () => {
dispatch(() => {
axios.get(GET_USERS_URL)
.then((response) => {
dispatch({ type: GET_USERS_SUCESS, users: response.data })
})
.catch((error) => {
dispatch({ type: GET_USERS_FAIL, error })
})
})
}
});
export default connect(mapStateToProps, mapDispatchToProps)(UserList);
Saga 处理流程 ☟
Saga 在业务组件中 dispatch
一个获取数据的 action
命令,然后 saga
监听到该 action
之后再去获取数据。
/container/PostList.js
import { connect } from 'react-redux';
import axios from 'axios';
import { GET_POSTS_SAGA } from 'constant/actionTypes';
import PostList from 'component/PostList';
const mapStateToProps = (state) => ({
posts: state.posts
});
const mapDispatchToProps = (dispatch) => ({
fetchPosts: () => dispatch({ type: GET_POSTS_SAGA })
});
export default connect(mapStateToProps, mapDispatchToProps)(PostList);
/redux/sagas/index.js
import { takeEvery, takeLatest } from 'redux-saga';
import { call, put } from 'redux-saga/effects';
import axios from 'axios';
import {
GET_POSTS_SAGA,
GET_POSTS_SUCCESS,
GET_POSTS_FAIL
} from 'constant/actionTypes';
import {
GET_POSTS_URL
} from 'constant/url';
// worker saga
function* showPostsAsync(action) {
try {
const response = yield call(axios.get, GET_POSTS_URL);
yield put({ type: GET_POSTS_SUCCESS, posts: response.data });
} catch(e) {
yield put({ type: GET_POSTS_FAIL, error: e });
}
}
// watcher saga
function* watchGetPosts() {
yield takeLatest(GET_POSTS_SAGA, showPostsAsync);
}
// root saga
export default function* rootSaga() {
yield watchGetPosts()
}
(4)Reducer 进行状态处理,返回新的 state
Thunk 和 Saga 在异步获取数据之后都会再 dispatch
一个 action
,然后,reducer
根据原有的 state 和 该 action
返回新的 state
。
(previousState, action) => newState
The End.