NPM versionRedux 全局状态管理 - 图2 npm bundle size (minified + gzip)

redux 小程序适配方案。在小程序开发中使用 redux 管理全局状态。

尽管小程序入门门槛非常之低,但是在项目不停的迭代过程中,不可避免的项目代码复杂度也会越来越高,从前我们可以将跨页面数据管理在Storage或者SessionStorage中,利用一定的代码规范来管理不同页面不同开发者的数据,但随着时间的推移这种方式会造成代码、数据过于分散,且容易出错覆盖。再者每个页面间都需要手动的去 storage 读取数据,略显繁琐。

当项目开始变得复杂,我们想要统一的管理起状态数据,自动的同步、分发数据到需要的页面、组件(Reactive)。

安装

  1. # 使用npm安装
  2. npm i -S @wxa/redux
  3. # 使用yarn安装
  4. yarn add @wxa/redux

基本用法

挂载插件

app.js/app.wxa中挂载插件

  1. // app.js or app.wxa
  2. import {App, wxa} from '@wxa/core';
  3. // 引入插件方法
  4. import {wxaRedux, combineReducers} from '@wxa/redux'
  5. import promiseMiddleware from 'redux-promise';
  6. // 注册插件
  7. wxa.use(wxaRedux, {
  8. reducers: combineReducers(...your reducer),
  9. middlewares: [promiseMiddleware]
  10. })
  11. @App
  12. export default class Main {};

注册完 redux 插件之后,将会自动的调用 redux.createStore 创建一个用于存储全局状态数据 store,并且插件会在自动的挂载 store 到 App、Component、Page 实例中 $store

通过 this.$store.getState()可以获得所有全局状态。

通过 this.$store.dispatch()可以提交一个状态修改的 action。

更详细的 store apiRedux 全局状态管理 - 图4

获取全局状态

在页面/组件类中定义 mapState 对象,指定关联的全局状态(在react中叫connect)。

  1. import {Page} from '@wxa/core';
  2. @Page
  3. export default class Index {
  4. mapState = {
  5. todolist$ : (state)=>state.todo,
  6. userInfo$ : (state)=>state.userInfo
  7. }
  8. add() {
  9. // dispatch change state.
  10. // todo list will auto add one.
  11. this.$store.dispatch({type: 'Add_todo_list', payload: 'coding today'});
  12. }
  13. }

然后再template中就可以直接使用映射的数据了。

  1. <view>{{userInfo$.name}}</view>
  2. <view wx:for="{{todolist$}}">{{key+1}}{{item}}</view>

得益于 @wxa/corediff方法,redux在同步数据的时候只会增量的修改数据,而不是全量覆盖 😁

在任意位置获取全局状态数据

编写一些通用的基础函数提供给页面调用的时候,可能会需要从 store 中读取相应数据做处理。 例如在我们需要在所有请求的 postdata 中统一的加上用户的基本信息,可以这么实现:

  1. // 任意 api.js
  2. import {fetch} from '@wxa/core`;
  3. import {getStore} from '@wxa/redux';
  4. export default const customFetch = (...args) => {
  5. let {idNo, name} = getStore().getState().UserModel;
  6. // 每个请求自动添加用户
  7. args[1] = {
  8. idNo, name
  9. ...args[1],
  10. };
  11. return fetch(...args);
  12. }

个性化页面数据

有时我们可能需要临时改写一下数据用于展示,实现类似 vue computed 的效果,此时我们可以相应的改造 mapState。

  1. export default class A {
  2. mapState = {
  3. userInfo$(state){
  4. let model = state.UserModel;
  5. // 自动掩码用户的身份证、姓名
  6. // diff 数据并自动调用 setData
  7. this.$diff({
  8. idNoCover: model.idNo.replace(/([\d]{4})(\d{10})([\dxX]{4})/, '$1***$3')
  9. })
  10. return model
  11. }
  12. }
  13. }

分包用法

当小程序应用开始使用分包技术Redux 全局状态管理 - 图5的时候,redux 方案也需要相应的做出优化,分包有以下特点:

引用原则

  • packageA 无法 require packageB JS 文件,但可以 require app、自己 package 内的 JS 文件
  • packageA 无法 import packageB 的 template,但可以 require app、自己 package 内的 template
  • packageA 无法使用 packageB 的资源,但可以使用 app、自己 package 内的资源

即当分包 A 定义了自己业务逻辑的数据 model 之后,且该 model 无法被其他分包复用,则我们完全可以把对应 model 放到分包的页面中,懒加载对应 redux.reducer,以此减少主包体积。

为了做到懒加载对应的 reducer,我们需要在改造一下我们的代码。

挂载插件

app.js/app.wxa中,改造 reducer 的注册方式。

  1. // app.js or app.wxa
  2. import {App, wxa} from '@wxa/core';
  3. // 引入插件方法
  4. import {wxaRedux, combineReducers} from '@wxa/redux'
  5. import promiseMiddleware from 'redux-promise';
  6. // 注册插件
  7. wxa.use(wxaRedux, {
  8. // reducers: combineReducers(...your reducer),
  9. reducers: {
  10. UserModel: userReducer,
  11. AppModel: appReducer,
  12. ...your reducer
  13. },
  14. middlewares: [promiseMiddleware]
  15. })
  16. @App
  17. export default class Main {};

动态添加分包 Reducer

假设我们在分包 A 中定义了专门用于订单处理的 reducer,分包入口页面为 subpages/A/pages/board

在分包页面被使用之前我们需要动态的注册一个新的 reducer

  1. // subpages/A/pages/board
  2. import {reducerRegistry} from '@wxa/redux';
  3. import AOrderModel from '/subpages/A/models/order.model.js';
  4. // 注册对应的数据 model
  5. reducerRegistry.register('AOrderModel', AOrderModel);

注册完毕之后,后续所有分包 A 的页面都可以正常的使用 mapState 中映射页面需要使用的状态数据。

调试 Redux

@wxa/redux 提供了小程序 redux-remote-devtools 的适配代码。稍微改造一下我们的挂载插件部分的代码即可使用:

  1. import {App, wxa} from '@wxa/core';
  2. // 引入插件方法
  3. import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux'
  4. import { composeWithDevTools } from '@wxa/redux/libs/remote-redux-devtools.js';
  5. import promiseMiddleware from 'redux-promise';
  6. const composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
  7. // 注册插件
  8. wxa.use(wxaRedux, {
  9. // reducers: combineReducers(...your reducer),
  10. reducers: {
  11. UserModel: userReducer,
  12. AppModel: appReducer,
  13. ...your reducer
  14. },
  15. middlewares: composeEnhancers(applyMiddleware(promiseMiddleware))
  16. })

打开开发者工具不校验合法域名开关,就可以正常使用 redux-devtools 了。

由于 devtools 仅用于开发阶段,我们可以利用 wxa 提供的依赖分析能力,按需引入。

改写上续配置如下:

  1. import {App, wxa} from '@wxa/core';
  2. // 引入插件方法
  3. import {wxaRedux, combineReducers, applyMiddleware} from '@wxa/redux'
  4. import promiseMiddleware from 'redux-promise';
  5. let composeEnhancers = (m) => m;
  6. if (process.env.NODE_ENV === 'production') {
  7. let composeWithDevTools = require('@wxa/redux/libs/remote-redux-devtools.js').composeWithDevTools;
  8. composeEnhancers = composeWithDevTools({ realtime: true, port: 8000 });
  9. }
  10. // 注册插件
  11. wxa.use(wxaRedux, {
  12. // reducers: combineReducers(...your reducer),
  13. reducers: {
  14. UserModel: userReducer,
  15. AppModel: appReducer,
  16. ...your reducer
  17. },
  18. middlewares: composeEnhancers(applyMiddleware(promiseMiddleware))
  19. })

如上配置,当 process.env.NODE_ENV 设置为生产环境的时候,@wxa/redux/libs/remote-redux-devtools.js 将不会被打包进 dist

持久化数据

某些场景,为了用户体验,我们需要将对应数据缓存下来,方便下次用户可以直接看到对应页面,此时我们需要将 store 的数据缓存下来,这里我们使用 redux-persistRedux 全局状态管理 - 图6 用于持久化数据。

示例如下:

  1. import {wxa, App} from '@wxa/core';
  2. import wxaRedux from '@wxa/redux';
  3. import wxPersistStorage from '@wxa/redux/libs/wx.storage.min.js';
  4. import {persistStore, persistReducer} from 'redux-persist';
  5. import orderModel from './order.model.js';
  6. let persistOrderModel = persistReducer({
  7. key: 'orderModel',
  8. storage: wxPersistStorage,
  9. timeout: null, // 超时时间,设置为 null
  10. }, orderModel);
  11. wxa.use(wxaRedux, {
  12. reducers: {
  13. orderModel: persistOrderModel
  14. }
  15. })
  16. @App
  17. export default class {
  18. onLaunch() {
  19. // 冷启动开始就加载缓存数据
  20. persistStore(this.$store, {}, ()=>this.$storeReady=true);
  21. }
  22. }

实时日志

我们可以结合小程序实时日志Redux 全局状态管理 - 图7redux-logger 一起使用。

  1. import {wxa, App} from '@wxa/core';
  2. import {createLogger} from 'redux-logger';
  3. import wxaRedux from '@wxa/redux';
  4. let log = wx.getRealtimeLogManager ? wx.getRealtimeLogManager() : console;
  5. let logger = createLogger({
  6. logger: log
  7. });
  8. wxa.use(wxaRedux, {
  9. reducers: {...your reducers},
  10. middlewares: [logger]
  11. });

配置完毕之后,项目中所有的 Action 日志都将上报到微信的实时日志后台,开发者可以登录 mp.weixin.qq.com 查看用户所有操作记录。

配置

reducers

  • 类型:
    • Function combineReducers(...reducer)的返回
    • Object reducer 列表,用于动态注册场景

middlewares

  • 类型:
    • Array redux 中间件列表
    • Function applyMiddleware(...middlewares)的返回

initialState

debug

  • 类型:
    • Boolean false

是否打印插件日志

技术细节

wxa/redux根据不同的实例类型有不同的任务,在App层,我们需要创建一个store并挂载到app中,在PageComponent层,我们做了更多细节处理。

  • App Level
    创建store,应用redux的中间件,挂载store到App实例。

  • Page Level
    在不同的生命周期函数,有不同的处理。

    • onLoad 根据mapState订阅store的数据,同时挂载一个unsubscribe方法到实例。
    • onShow 标记页面实例$$isCurrentPagetrue, 同时做一次状态同步。因为有可能状态在其他页面做了改变。
    • onHide 重置$$isCurrentPage,这样子页面数据就不会自动刷新了。
    • onUnload 调用$unsubscribe取消订阅状态
  1. Component Level
    针对组件生命周期做一些单独处理
    • created 挂载store
    • attached 订阅状态,并同步状态到组件。
    • detached 取消订阅