【微信小程序】---- redux 在原生微信小程序的使用实例

本文详细介绍了如何在微信小程序中使用weapp-redux,包括源码修改、store创建、action和reducers的组织,以及如何通过Provider封装实现全局状态管理。重点讲解了动态更新策略和性能优化方法。

weapp-redux 下载

  1. weapp-redux 使用实例下载

预览

在这里插入图片描述

开发

1. 目标

  1. 学会 redux 在原生微信小程序的使用;
  2. 学习和思考微信小程序中封装 Provider;

2. 引入 redux 文件

  1. 下载 redux
  2. git 克隆
	git clone https://github.com/reduxjs/redux.git
  1. 使用实例【 weapp-redux-demo 】下载
    在这里插入图片描述

3. 修改 redux 源码

import createStore from './redux/createStore';
import combineReducers from './redux/combineReducers';
import compose from './redux/compose';
import applyMiddleware from './redux/applyMiddleware';
import bindActionCreators from './redux/bindActionCreators';

export {
  createStore, 
  combineReducers, 
  applyMiddleware, 
  bindActionCreators,
  compose
};

修改 redux 源码适配微信小程序!

4. 创建 store

在这里插入图片描述

1. 创建 store
  1. store/index.js
import { createStore, applyMiddleware } from '../weapp-redux/index'

import reducer from './reducers/index';

const store = createStore(reducer)
export default store;
2. 创建 reducers

在这里插入图片描述

  1. reducers/index.js
import { combineReducers } from '../../weapp-redux/index';

import { commonCart } from './cart';
import { goodsDetail } from './goodsDetail';
import { integralZone } from './integralZone';
import { collect } from './collect';

export default combineReducers({
  commonCart,
  goodsDetail,
  integralZone,
  collect
})
  1. 页面示例数据创建 [reducers/collect.js]
import {
  UPDATE_COLLECT_LIST,
  CHECK_COLLECT_STATUS_GOODS
} from '../constants/actionTypes'

const defaultState = {
  list: [],
  goodsCollectStatus: {}
}

export const collect = (state = defaultState, action) => {
  switch (action.type) {
    case UPDATE_COLLECT_LIST:
      return { ...state, list: action.list };
    case CHECK_COLLECT_STATUS_GOODS:
      return { ...state, goodsCollectStatus: action.status}
    default:
      return state;
  }
}
3. 创建 constants

在这里插入图片描述

  1. 创建命令常量示例 constants/actionTypes.js
// 收藏商品
// 收藏列表更新
export const UPDATE_COLLECT_LIST = 'UPDATE_COLLECT_LIST_GOODS';
// 删除收藏商品
export const DELETE_COLLECT_GOODS = 'DELETE_COLLECT_GOODS';
// 添加收藏商品
export const ADD_COLLECT_GOODS = 'ADD_COLLECT_GOODS';
// 收藏状态查询
export const CHECK_COLLECT_STATUS_GOODS = 'CHECK_COLLECT_STATUS_GOODS';
4. 创建 actions

在这里插入图片描述

  1. actions/index.js
import * as integral from './integralZone';
import * as goodsDetail from './goodsDetail';
import * as collect from './collect';

export default {
  integral,
  collect,
  goodsDetail
}

注意此处暴露接口命名最好和reducers中保持一直,这样不易名字混淆!

  1. actions中更新页面示例[actions/collect.js]
import store from '../index'
import axios from '../../request/axios'
import { 
  UPDATE_COLLECT_LIST,
  CHECK_COLLECT_STATUS_GOODS
} from '../constants/actionTypes';

export function updateCollectList(list){
  store.dispatch({
    type: UPDATE_COLLECT_LIST,
    list
  })
}
export function checkCollectStatusGoods(status){
  store.dispatch({
    type: CHECK_COLLECT_STATUS_GOODS,
    status
  })
}
// 获取用户收藏列表
export function getCollectList(){
  axios.collectList().then((list = []) => {
    if(Array.isArray(list) && list.length){
      // 所有收藏商品为未选中
      list.forEach(cur => cur.ischeck = false);
      updateCollectList(list);
    }
  }).catch(console.log)
}
// 获取商品收藏状态
export function getGoodsCollectStatus(goodsId, goodsType){
  axios.getGoodsCollectStatus({goodsId, goodsType}).then(checkCollectStatusGoods).catch(console.log);
}
// 添加收藏商品
export function addGoodsCollect(goodsId, skid){
  return new Promise((resolve, reject) => axios.addGoodsCollect({goodsId, skid}).then(resolve).catch(reject));
}
// 删除收藏商品
export function deleteGoodsCollect(id){
  return new Promise((resolve, reject) => axios.delCollect({id}).then(resolve).catch(reject));
}

5. 在 app.js 中引入 store

5.1 直接引入 store 作为 app 的全局变量,页面使用直接 [getApp().store] 进行访问
// app.js
import store from './utils/store/index'
App({
	store
})
5.1.1 优点
  1. 引入少;
  2. 操作不频繁;
  3. 对于每个页面,有一个干净的全局变量空间;
5.1.2 缺点
  1. 更新繁琐,不会自动更新涉及变量的所有位置;
  2. 需要手动在需要的时候获取变量,效果等同于将变量放在app.js;
  3. 操作繁琐,必须手动获取 app.js 中的 store 来获取变量;
5.2 根据 5.1 的缺点思考改进

封装一个类似 react-redux 的中间件;

5.2.1 封装 Provider
  1. 思考如何动态更新?
  2. 如何减少更新通知?
  3. 如何仅更新部分更新的数据,不变的数据不更新?
1. 动态更新
  1. 进行页面 Page 和组件 Component 的拦截;
  2. 在页面和组件加载时,对当前页面 进行订阅 subscribe;
  3. 注意页面和组件卸载时,需要取消订阅;
  4. 不是所有的页面和组件都需要订阅,定义变量,判断当前页面是否需要订阅;
  5. 获取订阅的全局页面变量;
export default function Provider(store){
  const originalPage = Page;
  const originalComponent = Component;
}
2. 页面的订阅和取消订阅
  1. 将 store 设置未页面的 s t o r e 变 量 , 方 便 t h i s . store 变量,方便 this. store便this.store 访问;
  2. storeTypes 存放当前页面需要订阅的全局状态;
  3. 调用 store 的订阅函数 subscribe,同时保存取消订阅方法 unsubscribe;
  4. 在订阅方法中获取当前页面需要订阅的全局状态,收集;
  5. 由于微信小程序的逻辑层和视图层通信需要使用 setData 函数,但是调用太频繁,消耗性能,因此收集需要订阅的全局状态,统一将数据通知视图层。
  6. 注意:必须初始化派送依次数据,否则页面初始化是没有数据的。
  7. 最后在页面卸载函数中监听 unsubscribe 是否存在,存在就在页面卸载的时候执行unsubscribe函数。
Page = (config = {}) => {
    const { onLoad, onUnload,storeTypes = [] } = config;
    config.onLoad = function(opts){
      // 监听全局状态数据变化
      // 设置页面栈变量$store
	  this.$store = store;
	  // 设置监听
	  if(storeTypes && storeTypes.length > 0){
	    this.unsubscribe = store.subscribe(() => {
	      let pageData = {}
	      storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {}))
	      this.setData(pageData)
	    })
	    // 初始化页面数据
	    store.dispatch({type: `@@redux/INIT${randomString()}`});
	  }
      // 绑定页面生命周期
      onLoad && onLoad.call(this, opts);
    }
    config.onUnload = function(){
      onUnload && onUnload.call(this);
      // 页面卸载,取消订阅
      this.unsubscribe && this.unsubscribe();
    }
    originalPage(config);
  }

注意:

  1. 判断 storeTypes,是判断页面是否订阅全局状态的依据,减少订阅的次数,因为每次订阅 listeners,都会收集,执行依次派发, listeners 中的订阅都会执行一次,全部页面和组件都订阅,会消耗性能过大,仅在需要的页面订阅 storeTypes,优化订阅次数。
  2. 订阅生成,但是如果不取消,就会一直存在,在修改全局状态时,会执行 listeners 中所有的订阅。但是页面卸载后下次进入就会生成新的 id,新的页面,所以需要重新订阅。因此需要在卸载页面的时候取消订阅 this.unsubscribe && this.unsubscribe()。

思考:

  1. 由于订阅后,派发时所有收集订阅都会执行,是否可以标记订阅,仅通知当前修改的全局状态存在的订阅,不存在当前修改状态的订阅不派发?
  2. setData 可以只更新部分修改的变量,不修改全部的变量。是否可以通过对比订阅修改前页面当前状态和全局状态进行对比,筛选局部修改变量,进行 setData 修改?
3. 组件的订阅和取消订阅

原理和页面一样,此处不多做解释,直接代码。

Component = (config = {}) => {
    const { lifetimes = {}, attached:conattached, detached:condetached, storeTypes  = [] } = config;
    const { attached, detached } = lifetimes;
    config.lifetimes = {
      attached:function(){
        // 监听全局状态数据变化
        // 设置页面栈变量$store
		  this.$store = store;
		  // 设置监听
		  if(storeTypes && storeTypes.length > 0){
		    this.unsubscribe = store.subscribe(() => {
		      let pageData = {}
		      storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {}))
		      this.setData(pageData)
		    })
		    // 初始化页面数据
		    store.dispatch({type: `@@redux/INIT${randomString()}`});
		  }
        // 绑定页面生命周期
        attached && attached.call(this);
        conattached && conattached.call(this);
      },
      detached:function(){
        detached && detached.call(this);
        condetached && condetached.call(this);
        // 组件卸载,取消订阅
        this.unsubscribe && this.unsubscribe();
      }
    }
    originalComponent(config);
  }
4. 完整 Provider 代码

import isPlainObject from '../utils/isPlainObject';
const randomString = () => Math.random().toString(36).substring(7).split('').join('.');
const subscribeInit = (config, store, page) => {
  let { storeTypes = [] } = config;
  // 设置页面栈变量$store
  page.$store = store;
  // 设置监听
  if(storeTypes && storeTypes.length > 0){
    page.unsubscribe = store.subscribe(() => {
      let pageData = {}
      storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {}))
      page.setData(pageData)
    })
    // 初始化页面数据
    store.dispatch({type: `@@redux/INIT${randomString()}`});
  }
}
export default function Provider(store){
  if(!store && typeof store !== 'object'){
    throw new Error(`Expected the root store to be a object.`)
  }
  const originalPage = Page;
  const originalComponent = Component;
  Page = (config = {}) => {
    const { onLoad, onUnload } = config;
    config.onLoad = function(opts){
      // 监听全局状态数据变化
      subscribeInit(config, store, this);
      // 绑定页面生命周期
      onLoad && onLoad.call(this, opts);
    }
    config.onUnload = function(){
      onUnload && onUnload.call(this);
      // 页面卸载,取消订阅
      this.unsubscribe && this.unsubscribe();
    }
    originalPage(config);
  }
  Component = (config = {}) => {
    const { lifetimes = {}, attached:conattached, detached:condetached } = config;
    const { attached, detached } = lifetimes;
    config.lifetimes = {
      attached:function(){
        // 监听全局状态数据变化
        subscribeInit(config, store, this);
        // 绑定页面生命周期
        attached && attached.call(this);
        conattached && conattached.call(this);
      },
      detached:function(){
        detached && detached.call(this);
        condetached && condetached.call(this);
        // 组件卸载,取消订阅
        this.unsubscribe && this.unsubscribe();
      }
    }
    originalComponent(config);
  }
  return {
    Page,
    Component
  }
}

6. 实际开发中的应用

  1. 引入页面或组件需要使用的action;
  2. 引入页面或组件需要使用的全局状态storeTypes;
  3. 逻辑层使用action中的方法;
// collect.js
import {
  getCollectList,
  addGoodsCollect,
  deleteGoodsCollect
} from '../../utils/store/actions/collect'
Page({
  storeTypes: ['collect'],
  data: {},
  onLoad(){
    getCollectList();
  }
})
// collect.wxml
<view class="rui-mb15" wx:for="{{ collect.list }}" wx:key="collectList">
 <rui-swipe-cell right-width="{{ right }}" disabled="{{isEdit}}">
   <view class="rui-form-li rui-flex-ac">
     <radio color="#e24c4e" 
     class="rui-mr15" 
     data-item="{{item}}"
     data-index="{{index}}"
     catchtap="chooseGoods" 
     checked="{{item.check}}" 
     wx:if="{{isEdit}}"></radio>
     <view class="rui-fa rui-pr rui-mr40" catchtap="routeTo" data-url="../goodsDetail/goodsDetail?goodsId={{item.tyfogoodsid}}&isFromBc={{item.goodsType}}" data-type="{{item.goodsType}}" data-id="{{item.tyfogoodsid}}">
       <view class="rui-now-btn" wx:if="{{item.isPot == 1}}">现货</view>
       <image lazy-load="true"  src="{{item.mainpic}}" class="rui-goods-img"></image>
     </view>
     <view class="rui-fg" catchtap="routeTo" data-url="../goodsDetail/goodsDetail?goodsId={{item.tyfogoodsid}}&isFromBc={{item.goodsType}}" data-type="{{item.goodsType}}" data-id="{{item.tyfogoodsid}}">
       <view class="rui-fs28 rui-line3 rui-color4 rui-collect-title">{{item.goodsname}}</view>
       <!-- <view class="rui-fs24 rui-color4 rui-mt20 rui-mb20">蓝色</view> -->
       <view class="rui-mt30">
         <text class="rui-fs30 rui-color17 rui-mr20">¥{{item.marketprice}}</text>
         <text class="rui-fs20" wx:if="{{showMoney}}">最高赚<text class="rui-color17">{{item.predictAmt}}</text>元</text>
       </view>
     </view>
   </view>
   <view slot="right" class="rui-cancel-collect" data-item="{{item}}" catchtap="cancelCollect">取消收藏</view>
 </rui-swipe-cell>
</view>

7. 总结

  1. 由于性能的原因,能够不使用,就尽量不使用;
  2. 除非多页面多组件同时使用了该全局状态,同时业务逻辑比较复杂,容易混淆,使用全局状态方便管理,否则不要设置为全局状态;
  3. 在订阅优化尽量只执行更新的订阅;
  4. setData 修改视图层数据尽量只修改局部改变的部分,而不是全部修改。

8. WXRUI体验二维码

WXRUI体验码

如果文章对你有帮助的话,请打开微信扫一下二维码,点击一下广告,支持一下作者!谢谢!

下载

我的博客,欢迎交流!

我的优快云博客,欢迎交流!

微信小程序专栏

前端笔记专栏

微信小程序实现部分高德地图功能的DEMO下载

微信小程序实现MUI的部分效果的DEMO下载

微信小程序实现MUI的GIT项目地址

微信小程序实例列表

前端笔记列表

游戏列表

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Rattenking

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值