Redux-CheatSheet

本文详细地阐述了 Redux 的设计理念与实践技巧,包含了其三大原则与简单的仿制、基础组件以及 React 集成使用、基于 Thunk, Promise, Sagas 三种不同的异步处理方式、Selector, Ducks 等其他常见的样式规范、中间件的实现原理与代码分析等。

Redux CheatSheet | Redux 设计理念与实践技巧清单

Redux 是受 Flux 启发的,类似于 Event Sourcing 的事件驱动型框架。

基础组件

实践工具

ducks

ducks-modular-redux 是对于 Ducks 规范的描述,其按照业务模块将 reducer, action, actionTypes 合并到单一的文件中。

// widgets.js
// Actions
const LOAD = 'my-app/widgets/LOAD';
const CREATE = 'my-app/widgets/CREATE';
const UPDATE = 'my-app/widgets/UPDATE';
const REMOVE = 'my-app/widgets/REMOVE';
// Reducer
export default function reducer(state = {}, action = {}) {
switch (action.type) {
// do reducer stuff
default: return state;
}
}
// Action Creators
export function loadWidgets() {
return { type: LOAD };
}
export function createWidget(widget) {
return { type: CREATE, widget };
}
export function updateWidget(widget) {
return { type: UPDATE, widget };
}
export function removeWidget(widget) {
return { type: REMOVE, widget };
}
// side effects, only as applicable
// e.g. thunks, epics, etc
export function getWidget () {
return dispatch => get('/widget').then(widget => dispatch(updateWidget(widget)))
}

A module...

MUST export default a function called reducer() MUST export its action creators as functions MUST have action types in the form npm-module-or-app/reducer/ACTION_TYPE MAY export its action types as UPPER_SNAKE_CASE, if an external reducer needs to listen for them, or if it is a published reusable library

在外部使用时,我们可以导出默认的 reducer:

import { combineReducers } from 'redux';
import * as reducers from './ducks/index';
const rootReducer = combineReducers(reducers);
export default rootReducer;

在组件中,可以导出所有的 Action:

import * as widgetActions from './ducks/widgets';

redux-actions

import { createActions, handleActions, combineActions } from 'redux-actions';
const defaultState = { counter: 10 };
const { increment, decrement } = createActions({
INCREMENT: (amount = 1) => ({ amount }),
DECREMENT: (amount = 1) => ({ amount: -amount })
});
const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
defaultState
);
const reducer = handleActions(
{
[combineActions(increment, decrement)](
state,
{
payload: { amount }
}
) {
return { ...state, counter: state.counter + amount };
}
},
defaultState
);
export default reducer;
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import thunkMiddleware from 'redux-thunk';
import logger from 'redux-logger';
const reducer = (state = {}, action) => {
...
}
const store = createStore(reducer, {}, applyMiddleware(
thunkMiddleware,
promiseMiddleware(),
logger,
));
export default store;
const promiseAction = () => ({
type: 'PROMISE',
payload: Promise.resolve()
});

Async Actions | 异步 Action 处理

Thunk

Promise

redux-promise 会自动处理 payload 为 Promise 对象的 Action,并且会分发该 Action 的副本,包含了 Promise 的处理结果,并且根据处理结果动态设置了 status 属性为 success 或者 error

// 创建简单的异步 Action
createAction('FETCH_THING', async id => {
const result = await somePromise;
return result.someValue;
});
// 与自定义的 WebAPI 协同使用
import { WebAPI } from '../utils/WebAPI';
export const getThing = createAction('GET_THING', WebAPI.getThing);
export const createThing = createAction('POST_THING', WebAPI.createThing);
export const updateThing = createAction('UPDATE_THING', WebAPI.updateThing);
export const deleteThing = createAction('DELETE_THING', WebAPI.deleteThing);

redux-promise-middleware 为我们提供了类似的异步处理功能,其能够接受某个 Promise,并且依次分发 Pending, Fulfilled, 以及 Rejected 这几个不同状态的 Action:

const promiseAction = () => ({
type: 'PROMISE',
payload: Promise.resolve()
});

该工具同样可以与 Redux Thunk 协同使用,来进行多个 Action 的分发:

const secondAction = data => ({
type: 'TWO',
payload: data
});
const first = () => {
return dispatch => {
const response = dispatch({
type: 'ONE',
payload: Promise.resolve()
});
response.then(data => {
dispatch(secondAction(data));
});
};
};

我们也可以自己实现 Promise 中间件,可以参考 redux/promiseMiddleware 的源代码实现。在实际应用中,我们往往需要等待某个操作处理结束进行刷新等附加响应;此时我们即可以创建自定义的 Thunk,也可以直接在类方法中使用 await 来等待 dispatch 函数返回的 Promise:

const { fetchThing } = bindActionCreators({ fetchThing }, dispatch);
// 这里即会在 Redux 中分发 Action,同样也会阻塞执行直至 Promise 处理完毕
await fetchThing().payload;

Sagas

Sagas 是源于计算机科学与技术的概念,用于描述事务及其关联处理操作的约束。redux-sagas 的官方描述是用于处理副作用(Side Effects)的 Redux 中间件,允许我们以同步地方式编写异步代码,并使用 try-catch 进行异常处理。

redux-saga-dataflow

与 redux-thunk 相比,redux-sagas 并不是直接从 UI 调用逻辑代码,而是进行纯粹地 dispatch action;所有的异步流程控制都被移入到了 sagas,从而增强组件与逻辑代码的可复用性与可测试性。redux-sagas 基于 ES6 Generator,能为我们提供高级的异步控制流以及并发管理,可以使用简单的同步方式描述异步流,并通过 fork 实现并发任务。

8cc1a873-c675-9009-570d-9684da4a704f

参考 fe-boilerplate/redux 的示例,我们首先需要引入并且创建 Sagas 中间件实例:

import createSagaMiddleware from 'redux-saga';
import rootSaga from '../sagas/sagas';
const sagaMiddleware = createSagaMiddleware();
// 引入中间件,并构建 Store 对象
...
sagaMiddleware.run(rootSaga);

这里的 sagas 文件,即包含了具体的逻辑处理代码:

// helloSaga 会在 sagaMiddleware.run 时即刻执行
export function* helloSaga() {
console.log('Hello Saga!');
}
// worker saga
export function* incrementAsync() {
yield call(delay, 1000);
// 继续分发事件
yield put({ type: 'SAGA_INCREMENT' });
}
// watcher saga
export function* watchIncrementAsync() {
// 监听 Action,并执行关联操作
yield takeEvery('SAGA_INCREMENT_ASYNC', incrementAsync);
}
// root saga
export default function* rootSaga() {
yield [helloSaga(), watchIncrementAsync()];
}

Sagas 为我们定义了三种不同的 Saga,其中 Worker Saga 负责 API 调用、异步请求、结果处理等具体的任务;Watcher Saga 则监听被 dispatch 的 actions,当接收到 action 或者知道其被触发时,调用 worker saga 执行任务。而 Root Saga 则是立即启动 Sagas 的唯一入口。Saga 使用 Generator 函数来 yield Effect,其利用生成器可以暂停执行,再次执行的时候从上次暂停的地方继续执行的特性。Effect 是一个简单的对象,该对象包含了一些给 middleware 解释执行的信息。可以通过使用 effects API, 如 fork,call,take,put,cancel 等来创建 Effect。

yield call(fetch, '/user')yield 了下面的对象,call 创建了一条描述结果的信息,然后,redux-saga middleware 将确保执行这些指令并将指令的结果返回给生成器:

{
type: CALL,
function: fetch,
args: ['/user']
}

我们也可以并发执行多个任务:

const [users, repos] = yield[(call(fetch, '/users'), call(fetch, '/repos'))];

同样以常见的接口请求,与结果处理为例:

import { take, fork, call, put } from 'redux-saga/effects';
// The worker: perform the requested task
function* fetchUrl(url) {
// 指示中间件调用 fetch 异步任务
const data = yield call(fetch, url);
// 指示中间件发起一个 action 到 Store
yield put({ type: 'FETCH_SUCCESS', data });
}
// The watcher: watch actions and coordinate worker tasks
function* watchFetchRequests() {
while (true) {
// 指示中间件等待 Store 上指定的 action,即监听 action
const action = yield take('FETCH_REQUEST');
// 指示中间件以无阻塞调用方式执行 fetchUrl
yield fork(fetchUrl, action.url);
}
}

样式风格

ducks

redux-actions

import { createActions, handleActions, combineActions } from 'redux-actions';
const defaultState = { counter: 10 };
const { increment, decrement } = createActions({
INCREMENT: (amount = 1) => ({ amount }),
DECREMENT: (amount = 1) => ({ amount: -amount })
});
const reducer = handleActions(
{
INCREMENT: (state, action) => ({
counter: state.counter + action.payload
}),
DECREMENT: (state, action) => ({
counter: state.counter - action.payload
})
},
defaultState
);
const reducer = handleActions(
{
[combineActions(increment, decrement)](
state,
{
payload: { amount }
}
) {
return { ...state, counter: state.counter + amount };
}
},
defaultState
);
export default reducer;
import { createStore, applyMiddleware } from 'redux';
import promiseMiddleware from 'redux-promise-middleware';
import thunkMiddleware from 'redux-thunk';
import logger from 'redux-logger';
const reducer = (state = {}, action) => {
...
}
const store = createStore(reducer, {}, applyMiddleware(
thunkMiddleware,
promiseMiddleware(),
logger,
));
export default store;
const promiseAction = () => ({
type: 'PROMISE',
payload: Promise.resolve()
});

Middleware | 中间件

实践的思考

现代 Web 开发/状态管理

Redux 适合于需要强项目健壮度与多人协调规范的大中型团队,对于很多中小型创业性质,项目需求迭代异常快的团队则往往可能起到适得其反的作用。如果你真的喜欢 Redux,那么更应该在合适的项目,合适的阶段去接入 Redux,而不是在需求尚未成型之处就花费大量精力搭建复杂的脚手架,说不准客户的需求图纸都画反了呢。Dan 推荐的适用 Redux 的情况典型的有:

仅就笔者的个人实践而言,在

对于数据的获取

对于简单可重复的全局状态,譬如通用的接口返回的错误信息,可以使用全局的错误状态:

function reducer(state, { payload }) {
if (payload.error) {
return {
...state,
errorMessage: payload.error.message
};
}
}

如果是复杂接口响应的处理,譬如创建或者更新的表单,特别是还需要包含大量的非跨组件的 UI 操作,那么不妨使用本地状态处理,当然也可以使用 Thunk 函数进行处理:

class Com extends Component {
async handleSubmit() {
try {
const result = await doMutation();
if (result.success) {
// 执行成功之后的界面操作
showSuccessMessage();
fetchThing();
closeModal();
} else {
// 执行失败之后的部分操作
doFallback();
}
} catch (e) {
showErrorMessage();
}
}
}

关键源代码

react-redux

在 Provider.js 中: