Why?
为什么要使用状态管理?
不论是开发小程序,H5,后台管理等等Web应用,总会存在多个 页面/组件 共用一些数据(状态)的情况。举一个常见的例子,在更新完用户信息后,在其他依赖用户信息的 页面/组件 也需要做到相应的数据更新,类似的一处修改。多处更新的场景比比皆是,这时候如果有一个数据中心管理器帮助我们做这些事情,就会很大程度提升开发效率和代码的可读性。最开始的时候我是在小程序的App.js中定义一个globalData的属性来存放这些数据,可是这些数据很容易被修改,所以我决定使用Redux来管理小程序的状态,让小程序的状态变得安全可控。
What?
mini-store
我结合Redux构建了一个小程序端的状态管理器,可以通过这个状态管理器很方便的管理小程序中的一些公共状态。mini-store
mixinPage
为了方便我自己的开发,我又写了个小程序页面公共页面混入方法mixinPage帮助我更好地处理一些页面的公共操作(比如注入store,分享等等)。
依赖项
必须依赖项: redux
基本必须依赖项: redux-thunk
处理异步acition
建议依赖项: runtime
用于使用async await
代码分析
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的生成:
完成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模式
不使用mixinPage
最后,我的这种写法肯定还是存在不少问题,希望大佬能够指正,也希望我的一些想法能对您带来帮助