Redux 是 JavaScript 状态容器,提供可预测化的状态管理。
存在多层数据流向时,可以用Redux进行管理。
引入依赖
npm install --save redux
npm install --save react-redux
npm install --save-dev redux-devtools-extension
Redux遵循的三个原则是什么?
- 单一事实来源: 整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。
- 状态是只读的: 改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
- 使用纯函数进行更改: 为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。
Redux工作流
1. 创建 Action 模块
返回一个action对象,用于dispatch()。改变内部 state 惟一方法是 dispatch 一个 action。
action只描述了有事情发生,并没有描述应用如何更新,可理解为新闻摘要,传入reducer告诉它要执行什么
约定action 内必须使用一个字符串类型的 type 字段来表示将要执行的动作
export type = {
SWITCH_MENU: 'SWITCH_MENU',
}
#Action Generator
export function switchMenu(menuName) {
return { // 返回一个action对象
type: type.SWITCH_MENU,
menuName
}
}
// 异步
export function asyncSwitchMenu(menuName) {
return dispatch => {
setTimeOut(() => dispatch(switchMenu(menuName)), 1000)
}
}
2. 创建 Reducer 模块
state
- 单一数据源:整个应用的state被存在一棵object tree中,一个应用只应该有一个数据源,保存在一个对象中,尽可能把state规范化,不存在嵌套。
- 状态是只读的:唯一用于改变state的方法就是触发action;没有直接修改原来的state,当 state 变化时需要返回全新的对象;
- 状态修改由纯函数完成
- 不要修改 state,返回新的state对象。 使用 Object.assign() 新建了一个副本。不能这样使用 Object.assign(state, { visibilityFilter: action.filter }),因为它会改变第一个参数的值。你必须把第一个参数设置为空对象。你也可以开启对ES7提案对象展开运算符的支持, 从而使用 { ...state, ...newState } 达到相同的目的。
- 在 default 情况下返回旧的 state。遇到未知的 action 时,一定要返回旧的 state
const initialState = {
menuName: "Home",
}
reducer
- 形式为 (state, action) => state 的
纯函数,只要是同样的输入,必定得到同样的输出。 - 描述了 action 如何把 state 转变成下一个 state。
- 当 state 变化时需要返回全新的对象,而不是修改传入的参数。
export default function (state = initialState, action) {
switch(action.type) {
case type.SWITCH_MENU:
return {... state, action,munuName}
}
}
如果有多个reducer,可以用combineReducer来合成一个createStore.
# reducers/index
import { combineReducers } from "redux";
export const reducer = combineReducers({
app,
events,
})
# reducer/app
export const app = combineReducers<appState>({
appProps: (
state = {
userInfo: { username: 'ibddp'},
country: country,
language: language,
}, // initialState
{ payload, type } // action
) => {
if (type !== app_UPDATE_PROPS) return state;
return { ...state, ...payload };
}
});
# action/app
export const appActions = {
updateProps: (payload) => dispatch => {
return dispatch({
type: app_UPDATE_PROPS,
payload: payload,
});
},
}
3. 创建 store 模块
创建 Redux store 来存放应用的状态
import { createStore, applyMiddleware } from 'redux';
import { composeWithDevTools } from 'redux-devtools-extension';
import thunk from 'redux-thunk'
import Reducer from '上面的reducer'
const composeEnhancers = composeWithDevTools({});
export default (prevState) => {
createStore(Reducer, composeWithDevTools())
// 或者 异步redux
createStore(Reducer, composeEnhancers(applyMiddleware(thunk)))
}
// API 是 { subscribe, dispatch, getState }。
let store = createStore(counter);
// 可以手动订阅更新,也可以事件绑定到视图层。
store.subscribe(() =>
console.log(store.getState())
);
// 改变内部 state 惟一方法是 dispatch 一个 action。
// action 可以被序列化,用日记记录和储存下来,后期还可以以回放的方式执行
store.dispatch({ type: 'INCREMENT' });
// 1
Store.Getstate()
获取store中的当前状态
Store.Subscribe()
每次dispatch便会执行一次。
注册一个监听者,一旦 State 发生变化,就自动执行subscribe中传入的 这个函数
// 每次 state 更新时,打印日志
// 注意 subscribe() 返回一个函数用来注销监听器
const unsubscribe = store.subscribe(() =>
console.log(store.getState()))
Store.Dispatch
改变内部 state 惟一方法是 dispatch 一个 action Store.dispatch(addToDo(‘xxxx’)) // 发起一系列 action store.dispatch(addTodo('Learn about actions'))
4. connect 方法连接React和Redux组件
connect()(NavLeft)
@connect()
class NavLeft extends React.Component {
}
connect有两个参数:
mapStateToProps()mapDispatchToProps()//action方法直接导出,不需要dispatch action
前者负责输入逻辑,即将state映射到 UI 组件的参数(props),mapStateToProps会订阅 Store (即subscribe(中)),每当state更新的时候,就会自动执行,重新计算 UI 组件的参数,从而触发 UI 组件的重新渲染。
后者负责输出逻辑,即将用户对 UI 组件的操作映射成.
5. 添加 Provider 作为根组件
connect方法生成容器组件以后,需要让容器组件拿到state对象,才能生成 UI 组件的参数。 一种解决方法是将state对象作为参数,传入容器组件。但是,这样做比较麻烦,尤其是容器组件可能在很深的层级,一级级将state传下去就很麻烦。
React-Redux 提供Provider组件,可以让容器组件拿到state,它的原理是React组件的context属性
<Provider store={store}>
<App />
</Provider>
调试工具
ReduxDevTools
中间件
redux-thunk
import thunk from 'redux-thunk';
const store = createStore(
reducer,
applyMiddleware(thunk)
);
上面代码使用redux-thunk中间件,改造store.dispatch,使得后者可以接受函数作为参数。
redux-thunk 的主要思想是扩展 action,使得 action 从一个对象变成一个函数。
// fetchUrl 返回一个 thunk
function fetchUrl(url) {
return (dispatch) => {
dispatch({ type: 'FETCH_REQUEST' });
fetch(url).then(data => dispatch({
type: 'FETCH_SUCCESS',
data
}));
}
}
// 如果 thunk 中间件正在运行的话,我们可以 dispatch 上述函数如下:
dispatch( fetchUrl(url) )
redux-thunk 的缺点:
(1)action 虽然扩展了,但因此变得复杂,后期可维护性降低;
(2)thunks 内部测试逻辑比较困难,需要mock所有的触发函数;
(3)协调并发任务比较困难,当自己的 action 调用了别人的 action,别人的 action 发生改动,则需要自己主动修改;
(4)业务逻辑会散布在不同的地方:启动的模块,组件以及thunks内部
redux-promise
让 Action Creator 返回一个 Promise 对象。
import promiseMiddleware from 'redux-promise';
const store = createStore(
reducer,
applyMiddleware(promiseMiddleware)
);
这个中间件使得store.dispatch方法可以接受 Promise 对象作为参数。这时,Action Creator 有两种写法。
- 返回值是一个 Promise 对象。
const fetchPosts =
(dispatch, postTitle) => new Promise(function (resolve, reject) {
dispatch(requestPosts(postTitle));
return fetch(`/some/API/${postTitle}.json`)
.then(response => {
type: 'FETCH_POSTS',
payload: response.json()
});
});
从上面代码可以看出,如果 Action 本身是一个 Promise,它 resolve 以后的值应该是一个 Action 对象,会被dispatch方法送出(action.then(dispatch)),但 reject 以后不会有任何动作;如果 Action 对象的payload属性是一个 Promise 对象,那么无论 resolve 和 reject,dispatch方法都会发出 Action。
React-Redux
简易React-Redux实现
createStore
export function createStore(reducer, enhancer) {
if(enhancer) {
return enhancer(createStore, reducer)
}
let currentState = {}
let currentListeners = []
function getState() {
return currentState
}
funciton subscribe(listener) {
currentListeners.push(listener)
}
function dispatch(action) {
currentState = reducer(currentState, action)
currentListener.forEach(v => v())
return action
}
dispatch({type: '@IMMOC/WONIU-REDUX'})
return {
getState, subscribe, dispatch
}
}
combineReducers
合并store
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(prevState, key) => {
prevState[key] = reducers[key](state[key], action);
return prevState;
},
{}
);
};
};
Provider
Provider把store放到context中,所有的子元素可以直接取到store
class Provider extends React.Componet {
static childContextTypes = {
store: PropType.object
}
constructor(props, context){
super(props, context)
this.store = props.store
}
getChildContext() {
return {store: this.store}
}
render() {
return this.props.children
}
}
connect
const connect =
(mapStateToProps=state=>state, mapDispatchToProps={}) => (WrapComponent) => {
return class ConnectComponent extends React.Componet {
static contextTypes = {
store: PropsTypes.object
}
constructor(props, context) {
super(props, context)
this.state= {
props: {}
}
}
update(){
// 获取mapStateToProps和mapDispatchToProps,放入this.props
const {store} = this.context;
const stateProps = mapStateToProps(store.getState())
const dispatchProps = bindActionCreator(mapDispatchToProps, store.dispatch)
this.setState({
props: {
...stateProps,
...dispatchProps,
...this.state.props
}
})
}
componentDidMount() {
const {store} = this.context
store.subscribe(() => this.update())
this.update()
}
render(){
return <WrapComponent {...this.state.props} />
}
}
}
function bindActionCreator(creator, dispatch) {
return (...args) => dispatch(creator(...args))
}
function bindActionCreators(creators, dispatch) {
let bound = {}
object.keys.forEach(v => {
let creator = creators[v]
bound[v] = bindActionCreator(creator, dispatch)
})
return bound
}
applyMiddleware()
function applyMiddleware(...middlewares) {
return createStore => (...args) => {
const store = createStore(...args)
let dispatch = store.dispatch
const midApi = {
getState: store.getState,
dispatch: (...args) => dispatch(...args)
}
middlewareChain = middlewares.map(middleware => middleware(midApi))
dispatch = compose(...middlewareChain)(store.dispatch)
return{
...store,
dispatch
}
}
}
compose()
export function compose(...funcs) {
if(funcs.length === 0){
return arg => arg
}
if(funcs.length === 1){
return funcs[0]
}
return funcs.reduce((res, item) => (...args) => res(item(...args)))
}
thunk
const thunk = ({dispatch, getState}) => next => action => {
if(typeof action === 'function'){
return action(dispatch, getState)
}
return next(action)
}
数据流设计
分割store及组合
方案一 功能维度子store
接入Store
# app.ts
import { Provider } from 'react-redux';
import { store } from 'store';
export const App = () => {
return (
<>
<DndProvider backend={HTML5Backend}>
<Provider store={store}>
<HashRouter>{renderRoutes(Config)}</HashRouter>
</Provider>
</DndProvider>
</>
);
};
定义Store
# store.ts
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import { RootReducer } from 'reducers';
const composeEnhancers = composeWithDevTools({});
export const store = createStore(RootReducer, composeEnhancers(applyMiddleware(thunk)));
Reducer
第一层RootReducer
#/reducers/index.ts
import { combineReducers } from 'redux';
import { ConfigReducer, ConfigState } from './config';
import { WorkbenchState, WorkbenchReducer } from './workbench';
export interface StoreState {
config: ConfigState;
workbench: WorkbenchState;
}
export const RootReducer = combineReducers<StoreState>({
config: ConfigReducer,
workbench: WorkbenchReducer,
});
第二三层,功能SubStore及细节层Reducer
手动定义Reducer
# /reducers/workbench.ts
# 手动定义
import { combineReducers } from 'redux';
import { SET_CANVAS_TYPE } from 'constant';
export interface WorkbenchState {
mode: 'view' | 'edit' | 'audit';
canvasType: 'event' | 'template';
}
export const WorkbenchReducer = combineReducers<WorkbenchState>({
mode: () => {
const { hash } = window.location;
if (hash.includes('edit')) return 'edit';
if (hash.includes('audit')) return 'audit';
return 'view';
},
canvasType: (state = 'event', { type, payload }) => {
if (type === SET_CANVAS_TYPE) {
return payload;
}
return state;
},
});
通用模式定义Reducer
# /reducers/config.ts
# 通用模式定义
import { combineReducers } from 'redux';
import { ActionConfig } from 'types';
import { genHttpReducer, HttpState } from './http';
import {
CONFIG_GET_ACTION_LIST,
CONFIG_GET_TCA_RELATION,
} from 'constant';
export interface ConfigState {
actionList: HttpState<ActionConfig[]>;
tacRelation: HttpState<{
[k: string]: {};
}>;
}
export const ConfigReducer = combineReducers<ConfigState>({
actionList: genHttpReducer(CONFIG_GET_ACTION_LIST, []),
tacRelation: genHttpReducer(CONFIG_GET_TCA_RELATION, {}),
});
# http.ts
import { HttpAction, HttpActionOptions } from 'actions';
import { HTTP_LOADING, HTTP_SUCCESS, HTTP_ERROR } from 'constant';
export interface HttpState<T> {
loading: boolean;
error: Error | null;
data: T;
options: HttpActionOptions;
firstPageLoading?: boolean;
}
export function genHttpReducer<T = any>(type: string, initValue: T | any, defaultLoading?: boolean) {
const init: HttpState<T> = {
loading: defaultLoading === undefined ? true : defaultLoading,
error: null,
data: initValue,
options: {} as HttpActionOptions,
};
return (state = init, action: HttpAction<T>) => {
const { payload } = action;
if (HTTP_LOADING + type === action.type) {
return { ...payload, data: initValue };
}
if (HTTP_SUCCESS + type === action.type) {
return { ...payload };
}
if (HTTP_ERROR + type === action.type) {
return { ...payload, data: initValue };
}
return state;
};
}
Action
手动定义
export class SmsCheckActions {
static getSmsCheckList(param) {
return async (dispatch, getState: () => StoreState) => {
const {
canvas: { detail },
} = getState();
// const dataForm = {
// canvasId: detail?.data?.id,
// }
// const res = (await http({
// method: 'POST',
// url: '/conversion/sms/risk/check',
// data: dataForm,
// params: {
// mispUserName: Storage.getUsername(),
// }
// })) as any;
// const { errno, data } = res;
// let _data = { ...data || {} };
// await dispatch({
// type: SMS_GET_CHECK_RISK_LIST,
// payload: _data,
// });
await dispatch({
type: SET_CANVAS_TYPE,
payload: 'template',
});
// return _data;
};
}
}
通用模式定义
export class ConfigActions {
// 获取action列表
static getActionList() {
return genHttpAction({
type: CONFIG_GET_ACTION_LIST,
url: `${BASE_URL.PREFIX}/action/actionList`,
});
}
// 获取TCA配置
static getTCARelationConfig(ids: string) {
return genHttpAction({
type: CONFIG_GET_TCA_RELATION,
url: `${BASE_URL.PREFIX}/namespace/tcaRelation`,
params: { businessId: ids },
});
}
}
# httpAction.ts
export interface HttpAction<T> {
type: string;
payload: {
options: HttpActionOptions;
loading: boolean;
error: Error | null;
data: T | null;
};
}
export interface HttpActionOptions extends AxiosRequestConfig {
type: string;
pipe?: (res: any) => any;
onSuccess?: (result: any) => any;
onError?: (e: Error) => any;
extra?: any;
}
export function httpLoading(options: HttpActionOptions) {
return {
type: HTTP_LOADING + options.type,
payload: { options, loading: true, error: null, data: null },
};
}
export function httpLoadingClose(options: HttpActionOptions) {
return {
type: HTTP_LOADING + options.type,
payload: { options, loading: false, error: null, data: null },
};
}
export function httpSuccess<T>(options: HttpActionOptions, result: T) {
return {
type: HTTP_SUCCESS + options.type,
payload: { options, loading: false, error: null, data: result },
};
}
export function httpError(options: HttpActionOptions, error: Error | null) {
return {
type: HTTP_ERROR + options.type,
payload: { options, loading: false, error, data: null, message: error?.message },
};
}
export function genHttpAction<T>(options: HttpActionOptions) {
return async (dispatch: Dispatch) => {
try {
dispatch(httpLoading(options));
let result = await http(options);
if (options.pipe) result = options.pipe(result);
dispatch(httpSuccess(options, result));
if (options.onSuccess) options.onSuccess(result);
return result;
} catch (e) {
console.error(e);
message.error(Err.getMessage(e));
dispatch(httpError(options, e));
if (options.onError) options.onError(e);
}
};
}
// static getPaxConfig() {
// return genHttpAction({
// type: CONFIG_GET_PAX_CONFIG,
// url: `${BASE_URL.POPE_PREFIX}/config/getAll`,
// pipe: res => {
// // apollo配置项源头处理
// const apolloConfig = { ...(res || {}) };
// const fission = apolloConfig?.fission || {};
// const res_temp = {};
// Object.keys(fission).forEach(item => {
// if (item === 'business_map') {
// res_temp[item] = LO.merge({}, apolloConfig[item], apolloConfig.other_business_map, fission[item]);
// } else if (item === 'country_map') {
// res_temp[item] = LO.merge({}, fission[item], apolloConfig[item]);
// } else {
// res_temp[item] = LO.merge({}, apolloConfig[item], fission[item]);
// }
// });
// if (res && res.feature_whitelist) {
// sessionStorage.setItem('feature_whitelist', JSON.stringify(res.feature_whitelist));
// }
// const _res: any = LO.merge({}, apolloConfig, res_temp); // 新处理完的数据高优先级进行合并
// return _res;
// },
// });
// }
触发
await dispatch(SMSCheckActions.getSmsCheckList('params'));
await dispatch({
type: SET_CANVAS_TYPE,
payload: 'template',
});
方案二 页面 + 系统公共配置 维度组合子store
将上述第二层 变成