封装一个在react上更易用的redux框架

卓宏达
2023-12-01

redux是一个用于管理状态的前端框架,不仅可以用于react,还可以用于vue。它的源码和设计思想,我们在《颤抖吧!一起手写一个redux框架!》已经讲过,没有看过的小伙伴们可以去看一下。

redux的核心是发明了store,通过dispatch来更改store里的值。

可redux要想在react上使用,需要把两者连接起来,这个时候react-redux就出现了。它的主要作用是:

  • componentDidMountstore.subscribe,这样在store.dispatch的时候就能回调subscribelistener
    具体看Provider.js中的源码实现:
import React, { Component } from 'react'
import PropTypes from 'prop-types'
import { ReactReduxContext } from './Context'

class Provider extends Component {
  //...

  componentDidMount() {
    this._isMounted = true
    this.subscribe()
  }

  subscribe() {
    const { store } = this.props

    this.unsubscribe = store.subscribe(() => {
      const newStoreState = store.getState()

      if (!this._isMounted) {
        return
      }

      this.setState(providerState => {
      	//小tip:return null代表不更新state
        if (providerState.storeState === newStoreState) {
          return null
        }

        return { storeState: newStoreState }
      })
    })

    // ...
  }

}

export default Provider

  • 提供了context,可以实现跨组件通信,而不用去状态提升。
    这其实用到了react自带的api:context
    具体实现是在Provider.js中声明context,并把store.getState()赋值给它。这样其子组件中就可以拿到context值,只不过要写一些模板代码,这些代码都封装在了connect.js中。这也就是为什么我们只要把我们的组件传给高阶组件connect中就可以获取到reduxstate
  • mapStateToProp用来把store中的state传给connect组件,所以在组件中可以通过this.props获取store中state的值。
  • mapDispatchToProps用来把用dispatch包裹的actionCreator传给组件(bindActionCreators中把dispatch给bind到actionCreator上),所以,在组件中通过props可以直接dispatch一个action。

以上大概就是要在react中使用redux要写的代码。
其实,react-redux封装的功能并不一定适合我们的业务场景,相反我们还多写了很多与业务无关的模板代码!
因此,我们可以尝试着自己去封装一个更易用的redux框架。它有如下几个特点:

  • 可以成功地连接react和redux。
  • 不需要在业务代码中手动bindActionCreator,也不需要写mapDispatchToProps和mapStateToProps。
  • 不用写完reducer,再去写action了。使用过react-redux的小伙伴们应该都发现了reducer和action实际上都是为了去更新数据层,却要写两次非常类似的代码。
    它的使用方法也很简单,只需要让你的页面继承这个basePage就可以了。
import React from "react";
import { createStore, applyMiddleware } from "redux";

export default class BasePage {
  constructor(props) {
    super(props);
    this.$actions = null;
    this.$store = null;
    this.state = {};
  }

  render() {
    let { View } = this;

    if (!View) return null;

    let pageContext = {
      page: this,
      state: this.state
    };
    return <View state={this.state} page={this} />;
  }

  get store() {
    if (this.$store) return this.$store;
    this.$store = this.createStore();
    return this.$store;
  }

  get actions() {
    if (this.$actions) return this.$actions;
    this.store; // trigger createStore
    this.$actions = {};
    if (!util.isObject(this.model)) return this.$actions;

    Object.keys(this.model).forEach(type => {
      this.$actions[type] = payload => this.store.dispatch({ type, payload });
    });
    return this.$actions;
  }

  createReducer() {
    return (state, action) => {
      let model = this.model;
      let nextState = state;
      if (util.isObject(model)) {
        let handler = reducer[action.type];
        if (util.isFunction(handler)) {
            nextState = handler(nextState, action.payload);
        }
      } else if (util.isFunction(model)) {
        nextState = reducer(nextState, action);
      }
      return nextState;
    };
  }

  createStore() {
    const middlewares = [];
    if (process.env.NODE_ENV === "development") {
      const reduxLogger = require("redux-logger");
      const logger = reduxLogger.createLogger();
      middlewares.push(logger);
    }
    const createStoreWithMiddleware = applyMiddleware(...middlewares)(
      createStore
    );
    let store = createStoreWithMiddleware(this.createReducer(), this.state);
    store.unsubscribe = store.subscribe(() => {
      let nextState = store.getState();
      this.setState(nextState);
    });
    Object.defineProperty(store, "actions", {
      get: () => this.actions
    });
    return store;
  }

可以看到,如上框架代码行数并不多,下面我来为大家来讲解一下。
首先,先看render(),在render中给每个view传入了page

<View state={this.state} page={this} />

这主要是用来在组件中调用action来更新state。
例如,我们在CounterView中调用INCRE_BY

export default function CounterView({ state, page }) {
  const { count } = state;

  return (
    <View>
      <TouchableOpacity onPress={()=> {
        page.actions.INCRE_BY
      }}>
        <Text style={{ backgroundColor: "red" }}>increase</Text>
      </TouchableOpacity>
    </View>
  );
}

此时页面才开始createStore。。或者说,只有调用页面第一个action来开始createStore。因为,在调用page.actions.INCRE_BY时,实际上会先调用

  get actions() {
    if (this.$actions) return this.$actions;
    this.store; // trigger createStore
    this.$actions = {};
    if (!util.isObject(this.reducer)) return this.$actions;

    Object.keys(this.reducer).forEach(type => {
      this.$actions[type] = payload => this.store.dispatch({ type, payload });
    });
    return this.$actions;
  }

此时,this.$actions实际上是空的,所以这个时候就会用this.model来构建action。这里的model不光可以构建action,还可以构建出reducer
我们来看下mode.js是怎样的:

export const INCRE = state => {
  let count = state.count + 1;
  return {
    ...state,
    count
  };
};

export const DECRE = state => {
  let count = state.count - 1;
  return {
    ...state,
    count
  };
};

export const INCRE_BY = (state, aaa = 2) => {
  let count = state.count + aaa;
  return {
    ...state,
    count
  };
};

看到这个model我相信你大致就能猜到我是如何通过model来构建reduceraction的了——构建action时,取出modelkey作为action名,并把每个functiondispatch包裹一下;构建reducer时,就直接调用model中相应keyfunction,注意这里每一个functionreturn一个新的state

而上面在调用action时,当发现store为空时,会接着去createStore

createStore() {
    const middlewares = [];
    if (process.env.NODE_ENV === "development") {
      const reduxLogger = require("redux-logger");
      const logger = reduxLogger.createLogger();
      middlewares.push(logger);
    }
    const createStoreWithMiddleware = applyMiddleware(...middlewares)(
      createStore
    );
    let store = createStoreWithMiddleware(this.createReducer(), this.state);
    store.unsubscribe = store.subscribe(() => {
      let nextState = store.getState();
      this.setState(nextState);
    });
    Object.defineProperty(store, "actions", {
      get: () => this.actions
    });
    return store;
  }

以上我们就完成了一个比react-redux更轻量级的redux框架,该框架使得我们能尽可能少地在业务代码中写无脑的模板代码。

最后,该框架是参考自@工业聚,他对于技术的态度,给了我很多前进的动力!

 类似资料: