在小程序开发中,对于全局共享的数据,我们可以直接配置在 App 中,然后在其它地方通过获取 App实例 的方式进行获取。
// app.js
App({
// 配置全局共享数据
globalData: { /** ... */ }
})
// xxx.js
const appInstance = getApp()
// 获取全局共享数据
console.log(appInstance.globalData)
这种方法能够满足我们的一些需求场景,但是也存在着诸多问题,例如共享数据的更改不方便追踪、数据更新后依赖的页面(组件)不能自动实时更新等。
Redux 是一个JS应用程序的可预测状态容器,很多Web开发中会用到它来作为全局状态管理工具,那么我们试着将它应用到小程序中。
Redux 本身只是一个状态容器,负责管理维护状态数据,我们需要实现一个绑定库,将 Redux 绑定到小程序中,实现数据的自动更新之类的功能。本篇文章不包含这部分实现的内容,先使用个人维护的绑定库(redux-miniprogram-bindings)进行使用方面的介绍,有兴趣的朋友可以先到 github 上了解具体的实现。
redux-miniprogram-bindings API 简单灵活,功能完善,内部有 批量队列更新 和 diff优化 处理,性能优异,目前 支持微信小程序 和 支付宝小程序。欢迎各位进行 Bug反馈,喜欢的朋友可以点个 Star 。
使用 redux-miniprogram-bindings 和 Redux 实现小程序的全局状态管理
安装
-
安装 Redux ,
redux-miniprogram-bindings本身只是一个绑定库,并不包含Redux的代码在内,需要单独安装 -
安装
redux-miniprogram-bindings将项目中 dist 目录下相应版本的 js 文件引入到项目中
// redux-miniprogram-bindings // 例如将 dist 目录下 redux-miniprogram-bindings.js 文件的内容拷贝到此处对于使用
npm的小程序项目,也可以使用npm或yarn进行安装npm i -S redux-miniprogram-bindings
使用
创建 Redux 的 Store 实例
这里简单创建一个 store,包含一个可以增减的 counter 计数器,和一个记录用户姓名、年龄的 userInfo 用户信息
// store.js
import { createStore, combineReducers } from 'redux'
function counter(state = 0, action) {
switch (action.type) {
case 'INCREMENT':
return state + (action.step || 1)
case 'DECREMENT':
return state - (action.step || 1)
default:
return state
}
}
const initUserInfo = { name: 'userName', age: 25 }
function userInfo(state = initUserInfo, action) {
switch (action.type) {
case 'SET_USER_INFO':
return { ...state, ...action.userInfo }
default:
return state
}
}
const rootReducer = combineReducers({ counter, userInfo })
const store = createStore(rootReducer)
export default store
设置 provider
设置 provider 必须在所有使用 store 的行为之前执行,但是对于 store 的使用行为往往遍布在代码的各处,最佳实践是在一个独立文件中执行 setProvider,在 app.js 文件的最顶部引入该文件
// setupStore.js
import { setProvider } from 'redux-miniprogram-bindings'
import store from 'your/store/path'
setProvider({
// store 实例
store,
// 命名空间
namespace:'',
// 是否开启了 component2,仅支付宝小程序设置该选项
component2: false,
})
// app.js
// 确保在其他代码之前
import './setupStore.js'
App({ /** ... */ })
在页面(组件)中使用
// 页面
import { $page } from 'redux-miniprogram-bindings'
import { actionCreator1, actionCreator2 } from 'your/store/action-creators/path'
$page({
// 这里的 counter 是 store 中状态的 key 值,本示例可选 counter、userInfo
// 字符串形式拿到的数据比较粗糙,但是页面只会在该 key 值发生改变时才会执行 setData 操作
mapState: ['counter'],
mapDispatch: {
methodsName1: actionCreator1,
methodsName2: actionCreator2,
},
})({
onLoad() {
// 读取 state 中的值
const { counter } = this.data
// dispatch actionCreator1
this.methodsName1()
// dispatch actionCreator2
this.methodsName2(/** ...args */)
},
})
// 组件
import { $component } from 'redux-miniprogram-bindings'
import { actionCreator1, actionCreator2 } from 'your/store/action-creators/path'
$component({
mapState: [
// 该函数接收 state 作为参数,返回任意对象
// 函数形式可以细化获取到状态数据,或者组装计算属性
// 但是会在每次状态变更时(并非是该组件依赖的状态)重新执行计算,然后 diff 比较后决定是否更新
// 所以建议函数尽量小而简单
(state) => ({
userName: state.userInfo.name,
}),
],
mapDispatch: (dispatch) => ({
methodsName1: () => dispatch(actionCreator1()),
methodsName2: (...args) => dispatch(actionCreator2(...args)),
}),
})({
attached() {
// 读取 state 中的值
const { userName } = this.data
// dispatch actionCreator1
this.methodsName1()
// dispatch actionCreator2
this.methodsName2(/** ...args */)
},
})
在页面或组件中 mapState 都可以是字符串或函数形式,更多情况下应该是两种形式的结合(简单状态数据使用字符串形式,复杂状态数据使用函数形式)
$page({
mapState: [
// 简单状态数据
'counter',
// 复杂、组合状态数据
(state) => ({
userName: state.userInfo.name,
}),
],
})({ /** ... */ })
mapState 中定义的数据会在状态变更时自动执行 diff ,然后判断需要更新后执行 setData 操作,触发视图更新。所以不需要在页面中使用的数据不建议写在此处,可以使用 useState().xxx 或者 useRef() 的方式进行获取
在页面或组件中 mapDispatch 都可以是对象或函数形式,对象形式会自动进行 dispatch 绑定,函数形式可以使用 dispatch 进行自定义组装。当然了,也可以完全不使用 mapDispatch,全部定义在外部也是可以的
在 XML 中使用
<view wx:if="{{ counter }}">{{ counter }}</view>
这里也可以使用 mapDispatch 中的方法进行事件绑定,但是此时需要注意,如果需要传递参数,请记得事件处理函数默认接收 event 对象作为第一个参数
<view bind:tap="handleAdd">Add</view>
快捷方式
以下是一些获取 store 实例对象属性、方法的快捷方式,建议使用,尤其是在支付宝小程序开启分包后一定要使用,不然会出现多 store 实例的错误问题(这是支付宝小程序分包机制的问题)
- 获取 store 实例对象
import { useStore } from 'redux-miniprogram-bindings'
const store = useStore()
- 获取当前状态对象
import { useState } from 'redux-miniprogram-bindings'
const state = useState()
// 相当于
import { useStore } from 'redux-miniprogram-bindings'
const store = useStore()
const useState = () => store.getState()
const state = useState()
- 获取 dispatch 方法
import { useDispatch } from 'redux-miniprogram-bindings'
const dispatch = useDispatch()
- 监听状态变化
import { useSubscribe } from 'redux-miniprogram-bindings'
// 启用监听
const unsubscribe = useSubscribe((currState, prevState) => {
// 细化监听哪些数据发生了改变
if (currState.userInfo.name !== prevState.userInfo.name) {
console.log('userName change')
}
})
// 取消监听
unsubscribe()
优化函数调用频率 useSelector
我们知道在 mapState 中函数形式会在每次状态变更时重新执行函数,这是不合理的,我们希望只有在我们依赖的状态数据发生改变时才需要执行,那么我们可以使用 useSelector 方式进行优化处理
import { $page, useSelector } from 'redux-miniprogram-bindings'
// 该函数只会在 counter 发生改变时重新执行
const countTextSelector = useSelector((state) => ({ countText: `${state.counter}次` }), ['counter'])
// 该函数只会在 userInfo 发生改变时重新执行
const userNameSelector = useSelector((state) => ({ userName: state.userInfo.name }), ['userInfo'])
$page({
mapState: [countTextSelector, userNameSelector],
})({ /** ... */ })
useSelector 需要明确知道状态的依赖项,如果设置错误的依赖项或者缺失了依赖项,会造成错误的更新行为,这里需要特别注意。不过很多时候如果函数足够的小和简单,并不需要该优化
获取某一状态的最新值 useRef
有些时候我们需要实时拿到某一状态的最新值
import { useState } from 'redux-miniprogram-bindings'
const selector = (state) => state.userInfo.name
const getUserName = () => selector(useState())
// 获取用户名
getUserName()
我们也可以使用内部提供的 useRef 方法实现
import { useRef } from 'redux-miniprogram-bindings'
const selector = (state) => state.userInfo.name
const userNameRef = useRef(selector)
setInterval(() => {
// userNameRef.value 永远是 state.userInfo.name 的最新值
console.log(userNameRef.value)
}, 1000)
useRef 也可以配合 useSelector 优化函数调用
扩展封装
可能提供的 connect、$page、$component 不是你喜欢的调用方式,或者自身业务已经扩展封装了相应的页面(组件)函数,那么可以通过设置 manual 属性实现自定义扩展封装
connect、$page、$component 都接收一个 manual 属性,该选项为 true 时不会自动调用 Page()、Component()方法,而是返回一个 整理好的 options 对象,可以再次包装处理调用
// bootstrap.js
import { connect } from 'redux-miniprogram-bindings'
const oldPage = Page
// 重写 Page
Page = function (options) {
const { mapState, mapDispatch, ...restOptions } = options
// 整理好的 options
const realOptions = connect({
mapState,
mapDispatch,
// 此处必须设置为手动挂载,因为已经重写了 Page 函数
manual: true,
})(restOptions)
oldPage(realOptions)
}
// app.js
// 引入扩展
import './bootstrap.js'
App({})
// 使用
Page({
mapState: [
// ...
],
mapDispatch: {
// ...
},
})
以上是对在小程序中使用 Redux 的简单介绍,更多详情请查看 redux-miniprogram-bindings