美文网首页微信小程序开发
小程序&Redux实现状态管理

小程序&Redux实现状态管理

作者: LeoMelody | 来源:发表于2019-05-28 17:11 被阅读6次

    Why?

    为什么要使用状态管理?

    不论是开发小程序,H5,后台管理等等Web应用,总会存在多个 页面/组件 共用一些数据(状态)的情况。举一个常见的例子,在更新完用户信息后,在其他依赖用户信息的 页面/组件 也需要做到相应的数据更新,类似的一处修改。多处更新的场景比比皆是,这时候如果有一个数据中心管理器帮助我们做这些事情,就会很大程度提升开发效率和代码的可读性。最开始的时候我是在小程序的App.js中定义一个globalData的属性来存放这些数据,可是这些数据很容易被修改,所以我决定使用Redux来管理小程序的状态,让小程序的状态变得安全可控。

    What?

    mini-store

    我结合Redux构建了一个小程序端的状态管理器,可以通过这个状态管理器很方便的管理小程序中的一些公共状态。mini-store

    mixinPage

    为了方便我自己的开发,我又写了个小程序页面公共页面混入方法mixinPage帮助我更好地处理一些页面的公共操作(比如注入store,分享等等)。

    依赖项

    必须依赖项: redux

    基本必须依赖项: redux-thunk 处理异步acition

    建议依赖项: runtime 用于使用async await

    代码分析

    demo--使用mixinPage

    demo--不使用mixinPage

    mini-store

    mixinPage

    mini-store

    mini-store主要由三部分组成:

    • 初始化store数据(注册store),这里我参考vuex的使用,在小程序中创建一个storeData的属性来收集这个页面所需要使用的store中的数据,用法如下:
    storeData: { // 在下面说到的mixinPage会解析这个属性
        user: { // user表示reducer的名称
            // 左边的userInfo表示在当前page中的属性
            // 右边的userInfo表示在reducer中的属性名
            userInfo: 'userInfo' 
        }
    }
    
    

    initStore代码,initStore主要是用来收集上面的storeData中声明的属性与store中reducer的属性的一一对应关系,code:

    /**
     * 这里我使用IIFE的写法只是为了方便我后期拓展,可以不使用这种写法
     * 获取每个页面storeData,并进行收集存储
     * 这个方法就是为了收集依赖的映射关系
     */
    export const initStoreData = (function() {
      return function(
        storeData = {}, // 页面中定义的storeData
        $store // 这里这样写主要是为了保证$store确实注入了,也可直接导入
      ) {
        const stateCache = new Map() // 缓存storeData中属性与store中值的对应关系
        const labelCache = new Map() // 缓存storeData中属性与store中属性的对应关系
        if (!$store)
          throw new Error(`can't find any store, please inject store first`)
        // 获取数据仓库初始数据
        const storeState = $store.getState()
        // 获取storeData中定义的本页面需要用的reducer
        const reducerList = Object.keys(storeData)
        // 遍历每一个reducer initData
        for (let reducer of reducerList) {
          const stateReducer = storeState[reducer]
          const dataReducer = storeData[reducer]
          if (stateReducer) {
            Object.keys(dataReducer).map(attr => {
              if (dataReducer[attr]) {
                // 记录store属性链和data中属性的映射
                labelCache.set(`${reducer}.${dataReducer[attr]}`, attr)
                // 记录store属性链和其初始值的映射
                stateCache.set(
                  `${reducer}.${dataReducer[attr]}`,
                  stateReducer[dataReducer[attr]]
                )
              }
            })
          } else {
            throw new Error(
              `can't find ${reducer} reducer, please define reducer before using`
            )
          }
        }
        return {
          labelCache,
          stateCache
        }
      }
    })()
    

    这部分代码写注释写起来有些复杂,图解一些上面的生成的两个cachemap的生成:

    生成两个cachemap

    完成initStore后,得到两个cachemap用于辅助后续的监听store变化中使用

    • 监听store变化,准确的说,这一步应该是赋初始值和监听store变化两部分
    /**
     * 监听页面store变化
     */
    export const listenStore = (function() {
    
      return function(caches, $store, ctx) {
        // 这两个cache就是在上面计算得到的两个cache
        const labelCache = caches.labelCache
        const stateCache = caches.stateCache
        // 先执行一次数据初始化
        ;(function() {
          const obj = {}
          for(let current of stateCache.keys()) {
            const stateValue = getValue($store.getState(), current)
            if (stateCache.get(current) !== stateValue) {
              stateCache.set(current, stateValue)
            }
            obj[labelCache.get(current)] = stateValue
          }
          ctx.setData(obj)
        })()
        // 注册监听器,state改动触发脏检查方法
        ctx._unsubscribe = $store.subscribe(() => {
          for(let current of stateCache.keys()) {
            const stateValue = getValue($store.getState(), current)
            if (stateCache.get(current) !== stateValue) {
              const obj = {}
              obj[labelCache.get(current)] = stateValue
              stateCache.set(current, stateValue)
              ctx.setData(obj)
            }
          }
        })
      }
    })()
    
    /**
     * 解析链式属性值
     */
    function getValue(obj = {}, attrStr) {
      if (!attrStr) throw new Error("please use right attr")
      let attrs = attrStr.split(".")
      for (let attr of attrs) {
        obj = obj[attr]
      }
      return obj
    }
    
    
    • 卸载store 当前页面在卸载时,其对应的监听器也要一并卸载
    /**
     * 监听页面卸载
     */
    export const unInstallListener = (function() {
      return function(ctx) {
        ctx._unsubscribe && ctx._unsubscribe()
        ctx._unsubscribe = null
      }
    })()
    

    最后,这三个步骤触发的时间分别为:

    • initStore 小程序加载时。所有的页面均会触发自己的initStore
    • listenStore 当前页面onLoad时触发
    • unInstallListener 当前页面onUnload时触发

    这些定义都在 mixinPage 中

    mixinPage

    mixinPage主要由两部分组成,含代码注释:

    1、baseOptions 基础配置项,用于配置所有页面的公共配置

    function baseOptions() {
      const result = {
        /**
        * 初始化数据,可以在这里设置页面都会存在的一些数据
        * 比如这里我的每个页面都需要一个isLoad和存储页面参数的options
        */
        data: {
          isLoad: false,
          options: {}
        },
    
        /**
         * 自定义方法,用于处理页面中onLoad之前要进行的操作
         * 页面加载前置处理
         */
        _beforeLoad(initResult, options) {
          this.data.isLoad = true
          this.data.options = options
          // 监听store数据变化
          listenStore(initResult, this.$store, this)
        },
    
        /**
         * 自定义方法,用于处理页面中onUnLoad之前要进行的操作
         * 页面卸载前置处理
         */
        _beforeUnLoad() {
          // 卸载store
          unInstallListener(this)
        },
    
        /**
         * 用户点击右上角分享
         * 不用每个页面都再去写一遍分享方法了
         */
        onShareAppMessage: function () {
          return {
            path: `/pages/index/index`
          }
        }
      }
        
      // 给页面绑定$store属性
      Object.defineProperty(result, '$store', {
        value: store,
        writable: false,
        configurable: true,
        enumerable: true
      })
      return result
    }
    
    

    2、混入属性方法,用户将页面中的属性和方法和上面的baseOptions做一个mixin

    /**
     * 混入属性方法, 
     * options 为SelfPage接收的
     */
    const mixinFn = (options) => {
    
      if (!options || typeof options !== 'object') {
        return baseOptions()
      }
      // 执行初始化Store操作,并获取到上面提到的两个cachemap
      const initResult = initStoreData(options.storeData, baseOptions().$store)
      // 将data属性做混入处理
      const data = {
        ...baseOptions().data,
        ...options.data || {}
      }
      // 除了data外其他属性,则直接进行替换处理
      options = {
        ...baseOptions(),
        ...options,
        data
      }
    
      const onLoad = options.onLoad
      const onUnload = options.onUnload
      
      // 集成_beforeLoad,做onLoad前置操作
      options.onLoad = function(options) {
        this._beforeLoad(initResult, options)
        onLoad && onLoad.call(this, options)
      }
        
      // 集成_beforeUnLoad,做onUnload前置操作
      options.onUnload = function() {
        this._beforeUnLoad()
        onUnload && onUnload.call(this)
      }
    
      return options
    }
    

    store

    这里就和正常的Redux中的store的写法一样,然后在mixinPage中引入这个store即可实现

    store/index.js 代码,这部分代码应该没什么说的了,下面两个reducer就是我demo中的。

    import { createStore, combineReducers, applyMiddleware} from '../libs/redux.js'
    import thunkMiddleware from '../libs/redux-thunk'
    import numberReducer from './reducer/number'
    import userReducer from './reducer/user'
    
    const allReducer = {
      number: numberReducer,
      user: userReducer
    }
    
    const rootReducer = combineReducers(allReducer)
    const store = createStore(rootReducer, applyMiddleware(thunkMiddleware))
    
    export default store
    

    actions

    actions用于定义行为,在redux中,state只能通过触发action来进行修改

    numberAction:

    export const INCREMENT_NUMBER = 'INCREMENT_NUMBER' // 增加数字
    export const DECREMENT_NUMBER = 'DECREMENT_NUMBER' // 减少数字
    
    /**
     * 增加数字action
     */
    export function incrementNumberAction(i = 1) {
      return {
        type: INCREMENT_NUMBER,
        num: i
      }
    }
    
    /**
     * 减少数字action
     */
    export function decrementNumberAction(i = 1) {
      return {
        type: DECREMENT_NUMBER,
        num: i
      }
    }
    
    
    

    userAction
    这个有异步处理方法

    import regeneratorRuntime from '../../libs/runtime'
    import {getUserInfo} from '../../apis/user.api'
    
    export const GET_USER_INFO = 'GET_USER_INFO' // 获取用户信息
    
    /**
     * 获取用户信息action
     */
    export function getUserInfoAction() {
      return async function(dispatch, getState) {
        const res = await getUserInfo()
        dispatch({
          type: GET_USER_INFO,
          payload: {
            userInfo: res.data
          }
        })
      }
    }
    

    reducers

    reducer用于在接收actions触发的行为后,对state做相应的修改

    numberReducer:

    import {
      INCREMENT_NUMBER,
      DECREMENT_NUMBER
    } from '../actions/number'
    
    const initialState = {
      number: 0
    }
    
    export default function(state = initialState, action) {
      switch(action.type) {
        case INCREMENT_NUMBER:
          return {
            ...state,
            number: state.number + action.number
          }
        case DECREMENT_NUMBER:
          return {
            ...state,
            number: state.number - action.number
          }
        default:
          return state
      }
    }
    

    demo

    这里使用万能的加减数字的状态管理,为了体现redux-thunk的重要性,我用easy-mock模拟一个获取用户信息的接口。可能有的小伙伴不喜欢用mixinPage这种模式,我也准备了不使用mixinPage的写法

    使用mixinPage模式

    demo--使用mixinPage

    不使用mixinPage

    demo--不使用mixinPage

    最后,我的这种写法肯定还是存在不少问题,希望大佬能够指正,也希望我的一些想法能对您带来帮助

    相关文章

      网友评论

        本文标题:小程序&Redux实现状态管理

        本文链接:https://www.haomeiwen.com/subject/jjadtctx.html