【RC02】一口气看完Redux (实现简单的Redux)

146 阅读6分钟

Redux 是 JavaScript 状态容器,提供可预测化的状态管理。 存在多层数据流向时,可以用Redux进行管理。

引入依赖

npm install --save redux

npm install --save react-redux

npm install --save-dev redux-devtools-extension

Redux遵循的三个原则是什么?

  1. 单一事实来源: 整个应用的状态存储在单个 store 中的对象/状态树里。单一状态树可以更容易地跟踪随时间的变化,并调试或检查应用程序。
  2. 状态是只读的: 改变状态的唯一方法是去触发一个动作。动作是描述变化的普通 JS 对象。就像 state 是数据的最小表示一样,该操作是对数据更改的最小表示。
  3. 使用纯函数进行更改: 为了指定状态树如何通过操作进行转换,你需要纯函数。纯函数是那些返回值仅取决于其参数值的函数。

Redux工作流

image.png

image.png

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;
  },
});

image.png

通用模式定义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, {}),
});

image.png

# 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

将上述第二层 变成