100 行代碼實現相似 dva 的 redux 簡易封裝

實現功能相似 dva、nuomi、rematch、mirror 的數據流管理方式

  • redux 原生寫法實現功能須要寫大量的 action、reducer 模板代碼,對於複雜項目來講模板代碼太多,且 action 文件和 reducer 文件等不停切換,開發體驗較差。你們通常都會封裝簡化。react

  • 好比 dva 的實現方式,引入了 effects,把 state、effects、action、reducer 放在一個 model 文件,簡化了 action 和 reducer 的定義方式。相似的還有 rematch,mirror。git

  • 今年項目重構時,因路由緩存等緣由,沒有采用現有 dva 等框架,簡易封裝實現了相似 dva 的 redux 數據流實現。github

簡單封裝實現下相似 api 和使用方式。redux

實現代碼

  • 命名 zoo,由於 z 字母開頭文件列表中在最底部,方便查找

直接上代碼,實現 redux 主體功能封裝api

import { createStore } from 'redux';
import { Provider } from 'react-redux';

// 爲 effects 或 reducer 添加 namespace, 方便保存到全局
const addNamespace = (obj, name) => {
  const newObj = {};
  Object.keys(obj).forEach(item => {
    newObj[`${name}/${item}`] = obj[item];
  });
  return newObj;
};

class Zoo {
  constructor() {
    // 定義公共 state、store、effects 等
    this.state = {};
    this.models = {};
    this.reducers = {};
    this.effects = {};
    this.store = {};
  }

  // zoo 初始化方法,傳入每一個模塊的 model
  init(models) {
    Object.values(models).forEach(item => {
      // 遍歷加載每一個 model
      this.model(item);
    });

    // 建立並返回全局 store
    return this.createStore();
  }

  // 加載模塊 model 方法
  model(modelObj) {
    const { state, reducer, effects, namespace } = modelObj;
    // 全局保存 state
    this.state[namespace] = state;
    this.models[namespace] = modelObj;

    // 保存 reducer
    const newReducer = addNamespace(reducer, namespace);
    this.reducers[namespace] = newReducer;

    // 保存 effects
    this.effects[namespace] = effects;
  }

  createStore() {
    // 合併 reducer, 建立 reducer 函數
    const reducer = (state = this.state, action) => {
      let newState = state;

      const { type, payload } = action;
      // 獲取每一個 action 的 namespace
      const [namespace, typeName] = type.split('/');

      // 根據 namespace 獲取對應 model 中 state 和 reducer 函數對象
      const currentState = newState[namespace];
      const currentReducer = this.reducers[namespace];

      // 若是 action 對應 reducer 存在,則根據函數修改 state,不然直接返回原 state
      if (currentReducer && currentReducer[type] && currentState) {
        // 根據 reducer 函數修改當前 namespace 的 state
        newState[namespace] = currentReducer[type](payload, currentState);
        // 修改後的 state 必須是新的對象,這樣纔不會覆蓋舊的 state,可使修改生效
        newState = { ...newState };
      }

      return newState;
    };

    // 調用 redux createStore 建立 store
    this.store = createStore(reducer);

    const { dispatch, getState } = this.store;

    /** * 給每一個 model 的 effects 對象添加全局 store 的 dispatch、getState 方法 * 用於在 effects 中調用 dispatch * 同時對 effects 中的方法名添加 namespace, 用於組件中 dispatch 時區分模塊 */
    Object.keys(this.effects).forEach(namespace => {
      this.effects[namespace].dispatch = ({ type, payload }) =>
        // 修改 action type,添加 namespace
        dispatch({ type: `${namespace}/${type}`, payload });
      this.effects[namespace].getState = getState;
    });

    return this.store;
  }
}

export default new Zoo();
複製代碼
  • connect 封裝
import React from 'react';
import { connect } from 'react-redux';
import zoo from './zoo';

// effectsArr 可做爲 effects 依賴注入使用
export default (mapState, mapDispatch = {}, effectsArr = []) => {
  return Component => {
    const { getState, dispatch } = zoo.store;

    // 修改組件中的 dispatch 默認先觸發 effects 中對應方法,不存在時做爲正常 action dispatch
    const myDispatch = ({ type, payload }) => {
      const [typeId, typeName] = type.split('/');
      const { effects } = zoo;

      if (effects[typeId] && effects[typeId][typeName]) {
        return effects[typeId][typeName](payload);
      }

      dispatch({ type, payload });
    };

    const NewComponent = props => {
      const { effects } = zoo;
      const effectsProps = {};
      // 組件中擴展加入 effects 對象,更方便調用 effects 中的方法
      effectsArr.forEach(item => {
        if (effects[item]) {
          effectsProps[`${item}Effects`] = effects[item];
          myDispatch[`${item}Effects`] = effects[item];
        }
      });

      return <Component {...props} dispatch={myDispatch} {...effectsProps} />; }; return connect(mapState, mapDispatch)(NewComponent); }; }; 複製代碼

如上,封裝後的 connect 擴展了不少功能,組件中得到的 dispatch 再也不僅僅觸發 action,而是直接 調用 effects 中的方法,更方便反作用處理,同時增長了 effects 依賴注入的接口(相似 Mobx 中的 inject)。緩存

zoo 實現完成,zoo 建立的 store 和 redux 原生建立的 store 並無區別。框架

zoo 使用

  • index.js
import { Provider } from 'react-redux';

import zoo from './zoo';
import todoModel from './zooExample/Todo/model';
import zooModel from './zooExample/Zoo/model';
import ZooExample from './zooExample/index';

// 只須要傳入各模塊 model 便可
const zooStore = zoo.init({
  todoModel,
  zooModel
});

render(
  <Provider store={zooStore}> <ZooExample /> </Provider>,
  document.getElementById('root')
);
複製代碼
  • model.js
export default {
  namespace: 'zoo',
  state: {
    list: []
  },
  effects: {
    setState(payload) {
      const state = this.getState();
      this.dispatch({ type: 'setState', payload: payload });
    },
    addAnimal(name) {
      const { list } = this.getState().zoo;
      this.setState({ list: [...list, name] });
    },
    async deleteOne() {
      const { list } = this.getState().zoo;
      const res = [...list];
      // 模擬異步請求操做
      setTimeout(() => {
        res.pop();

        this.setState({ list: res });
      }, 1000);
    }
  },
  reducer: {
    setState: (payload, state) => ({ ...state, ...payload })
  }
};
複製代碼
  1. 功能組件 ZooExample.js
import React, { useState, useEffect } from 'react';
import { connect } from '../../zoo';

const TestTodo = ({ dispatch, list, zooEffects }) => {
  const [value, setValue] = useState('');

  useEffect(() => {
    dispatch({ type: 'zoo/getAnimal' });
  }, []);

  const onAdd = () => {
    dispatch({
      type: 'zoo/addAnimal',
      payload: value
    });
  };

  const onDelete = () => {
    zooEffects.deleteOne();
    // 或 dispatch.zooEffects.deleteOne();
  };

  return (
    <div> <input onChange={e => setValue(e.target.value)} /> <button onClick={onAdd}>add animal</button> <button onClick={onDelete}>delete animal</button> <br /> <ul> {list.map((item, i) => { return <li key={item + i}>{item}</li>; })} </ul> </div> ); }; export default connect( state => { return { list: state.zoo.list }; }, {}, // effects 注入 ['todo', 'zoo'] )(TestTodo); 複製代碼

一個簡易的 redux 封裝就完成了,約 100 多行代碼,不須要寫 action,不須要寫 switch case,相比 dva 異步請求簡單,dispatch 功能強大,能夠實現 3 種方法觸發 effects 也能夠很簡潔。異步

nuomi 中 redux 的封裝原理基本一致async

以上代碼能夠正常使用,但 connect 方法的封裝存在 ref 穿透等細節問題ide

示例代碼倉庫 github.com/iblq/zoo

相關文章
相關標籤/搜索