天天看點

【微信小程式】---- redux 在原生微信小程式的使用執行個體

weapp-redux 下載下傳

  1. weapp-redux 使用執行個體下載下傳

預覽

【微信小程式】---- redux 在原生微信小程式的使用執行個體

開發

1. 目标

  1. 學會 redux 在原生微信小程式的使用;
  2. 學習和思考微信小程式中封裝 Provider;

2. 引入 redux 檔案

  1. 下載下傳 redux
  2. git 克隆
git clone https://github.com/reduxjs/redux.git           

複制

  1. 使用執行個體【 weapp-redux-demo 】下載下傳
【微信小程式】---- redux 在原生微信小程式的使用執行個體

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

【微信小程式】---- redux 在原生微信小程式的使用執行個體

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

【微信小程式】---- redux 在原生微信小程式的使用執行個體
  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

【微信小程式】---- redux 在原生微信小程式的使用執行個體
  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

【微信小程式】---- redux 在原生微信小程式的使用執行個體
  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 設定未頁面的

store 變量,友善 this.

store 通路;

  1. storeTypes 存放目前頁面需要訂閱的全局狀态;
  2. 調用 store 的訂閱函數 subscribe,同時儲存取消訂閱方法 unsubscribe;
  3. 在訂閱方法中擷取目前頁面需要訂閱的全局狀态,收集;
  4. 由于微信小程式的邏輯層和視圖層通信需要使用 setData 函數,但是調用太頻繁,消耗性能,是以收集需要訂閱的全局狀态,統一将資料通知視圖層。
  5. 注意:必須初始化派送依次資料,否則頁面初始化是沒有資料的。
  6. 最後在頁面解除安裝函數中監聽 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 修改視圖層資料盡量隻修改局部改變的部分,而不是全部修改。