小程序中使用Redux

780 阅读5分钟

在小程序开发中,对于全局共享的数据,我们可以直接配置在 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-bindingsRedux 实现小程序的全局状态管理

安装

  • 安装 Reduxredux-miniprogram-bindings 本身只是一个绑定库,并不包含 Redux 的代码在内,需要单独安装

  • 安装 redux-miniprogram-bindings

    将项目中 dist 目录下相应版本的 js 文件引入到项目中

    // redux-miniprogram-bindings
    // 例如将 dist 目录下 redux-miniprogram-bindings.js 文件的内容拷贝到此处
    

    对于使用 npm 的小程序项目,也可以使用 npmyarn 进行安装

    npm i -S redux-miniprogram-bindings
    

使用

创建 ReduxStore 实例

这里简单创建一个 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