Webapp Redux Download
preview

development
1. Objectives
- Learn how to use redux in native wechat applet;
- Learn and think about the encapsulated Provider in wechat applet;
2. Import redux file
- download redux
- git clone
git clone https://github.com/reduxjs/redux.git

3. Modify redux source code
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 };
Modify the redux source code to adapt to wechat applet!
4. Create a store

1. Create a store
- store/index.js
import { createStore, applyMiddleware } from '../weapp-redux/index' import reducer from './reducers/index'; const store = createStore(reducer) export default store;
2. Create reducers

- 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 })
- Page sample data creation [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. Create constants

- Create a command constant example constants / actiontypes js
// Collect goods // Favorite list update export const UPDATE_COLLECT_LIST = 'UPDATE_COLLECT_LIST_GOODS'; // Delete favorite items export const DELETE_COLLECT_GOODS = 'DELETE_COLLECT_GOODS'; // Add favorite items export const ADD_COLLECT_GOODS = 'ADD_COLLECT_GOODS'; // Collection status query export const CHECK_COLLECT_STATUS_GOODS = 'CHECK_COLLECT_STATUS_GOODS';
4. Create actions

- actions/index.js
import * as integral from './integralZone'; import * as goodsDetail from './goodsDetail'; import * as collect from './collect'; export default { integral, collect, goodsDetail }
Note that the name of the exposed interface here should be kept the same as that in reducers, so it is not easy to confuse the name!
- Update page example in 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 }) } // Get user favorite list export function getCollectList(){ axios.collectList().then((list = []) => { if(Array.isArray(list) && list.length){ // All favorite items are unselected list.forEach(cur => cur.ischeck = false); updateCollectList(list); } }).catch(console.log) } // Get collection status export function getGoodsCollectStatus(goodsId, goodsType){ axios.getGoodsCollectStatus({goodsId, goodsType}).then(checkCollectStatusGoods).catch(console.log); } // Add favorite items export function addGoodsCollect(goodsId, skid){ return new Promise((resolve, reject) => axios.addGoodsCollect({goodsId, skid}).then(resolve).catch(reject)); } // Delete favorite items export function deleteGoodsCollect(id){ return new Promise((resolve, reject) => axios.delCollect({id}).then(resolve).catch(reject)); }
5. On app Introducing store into JS
5.1 store is directly introduced as the global variable of app, and the page is accessed directly with [getApp().store]
// app.js import store from './utils/store/index' App({ store })
5.1.1 advantages
- Less introduction;
- Infrequent operation;
- For each page, there is a clean global variable space;
5.1.2 disadvantages
- The update is cumbersome and will not automatically update all positions involving variables;
- You need to manually obtain variables when needed. The effect is equivalent to putting variables in app js;
- The operation is cumbersome, and you must obtain the app manually JS to get variables;
5.2 think about improvement according to the shortcomings of 5.1
Encapsulate a middleware similar to react Redux;
5.2.1 package Provider
- Think about how to update dynamically?
- How to reduce update notifications?
- How to update only part of the updated data without updating the unchanged data?
1. Dynamic update
- Intercept Page and Component;
- subscribe to the current page when the page and component are loaded;
- Note that the subscription needs to be cancelled when the page and component are unloaded;
- Not all pages and components need to be subscribed. Define variables to determine whether the current page needs to be subscribed;
- Get the global page variable of the subscription;
export default function Provider(store){ const originalPage = Page; const originalComponent = Component; }
2. Subscribe to and unsubscribe from the page
- Set store to page not
store access;
- storeTypes stores the global status of the current page to be subscribed to;
- Call the store subscription function subscribe, and save the unsubscribe method unsubscribe;
- In the subscription method, get the global status of the current page to be subscribed and collect;
- Because the logic layer and view layer communication of wechat applet need to use the setData function, but the calls are too frequent, which consumes performance. Therefore, collect the global status that needs to be subscribed and uniformly notify the view layer of the data.
- Note: the sending sequence data must be initialized, otherwise there is no data for page initialization.
- Finally, listen for the existence of unsubscribe in the page unloading function, and execute the unsubscribe function when the page is unloaded.
Page = (config = {}) => { const { onLoad, onUnload,storeTypes = [] } = config; config.onLoad = function(opts){ // Monitor global status data changes // Set the page stack variable $store this.$store = store; // Set listening if(storeTypes && storeTypes.length > 0){ this.unsubscribe = store.subscribe(() => { let pageData = {} storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {})) this.setData(pageData) }) // Initialize page data store.dispatch({type: `@@redux/INIT${randomString()}`}); } // Binding page lifecycle onLoad && onLoad.call(this, opts); } config.onUnload = function(){ onUnload && onUnload.call(this); // Page uninstall, unsubscribe this.unsubscribe && this.unsubscribe(); } originalPage(config); }
be careful:
- Judging storeTypes is the basis for judging whether a page subscribes to the global state and reducing the number of subscriptions, because every time you subscribe to listeners, it will be collected and distributed in turn. The subscriptions in listeners will be executed once. All pages and components will subscribe, which will consume too much performance. Subscribe to storeTypes only on the required pages to optimize the number of subscriptions.
- Subscriptions are generated, but if they are not cancelled, they will always exist. When modifying the global state, all subscriptions in listeners will be executed. However, after the page is unloaded, the next entry will generate a new id and a new page, so you need to subscribe again. Therefore, you need to unsubscribe from this when uninstalling the page unsubscribe && this. unsubscribe().
reflection:
- Since all collection subscriptions will be executed during distribution after subscription, can you mark the subscription and only notify the existing subscriptions in the current modified global state, and the subscriptions that do not exist in the current modified state will not be distributed?
- setData can update only some modified variables, not all variables. Can I filter local modification variables and modify setData by comparing the current status of the page before subscription modification with the global status?
3. Subscription and unsubscribe of components
The principle is the same as that of the page. There is no more explanation here, but the code directly.
Component = (config = {}) => { const { lifetimes = {}, attached:conattached, detached:condetached, storeTypes = [] } = config; const { attached, detached } = lifetimes; config.lifetimes = { attached:function(){ // Monitor global status data changes // Set the page stack variable $store this.$store = store; // Set listening if(storeTypes && storeTypes.length > 0){ this.unsubscribe = store.subscribe(() => { let pageData = {} storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {})) this.setData(pageData) }) // Initialize page data store.dispatch({type: `@@redux/INIT${randomString()}`}); } // Binding page lifecycle attached && attached.call(this); conattached && conattached.call(this); }, detached:function(){ detached && detached.call(this); condetached && condetached.call(this); // Component uninstall, unsubscribe this.unsubscribe && this.unsubscribe(); } } originalComponent(config); }
4. Complete Provider code
import isPlainObject from '../utils/isPlainObject'; const randomString = () => Math.random().toString(36).substring(7).split('').join('.'); const subscribeInit = (config, store, page) => { let { storeTypes = [] } = config; // Set the page stack variable $store page.$store = store; // Set listening if(storeTypes && storeTypes.length > 0){ page.unsubscribe = store.subscribe(() => { let pageData = {} storeTypes.forEach(storeType => pageData[storeType] = (store.getState()[storeType] || {})) page.setData(pageData) }) // Initialize page data 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){ // Monitor global status data changes subscribeInit(config, store, this); // Binding page lifecycle onLoad && onLoad.call(this, opts); } config.onUnload = function(){ onUnload && onUnload.call(this); // Page uninstall, unsubscribe this.unsubscribe && this.unsubscribe(); } originalPage(config); } Component = (config = {}) => { const { lifetimes = {}, attached:conattached, detached:condetached } = config; const { attached, detached } = lifetimes; config.lifetimes = { attached:function(){ // Monitor global status data changes subscribeInit(config, store, this); // Binding page lifecycle attached && attached.call(this); conattached && conattached.call(this); }, detached:function(){ detached && detached.call(this); condetached && condetached.call(this); // Component uninstall, unsubscribe this.unsubscribe && this.unsubscribe(); } } originalComponent(config); } return { Page, Component } }
6. Application in actual development
- Actions to be used for importing pages or components;
- Global state storeTypes to be used for importing pages or components;
- The logic layer uses the methods in 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}}">goods in stock</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">blue</view> --> <view class="rui-mt30"> <text class="rui-fs30 rui-color17 rui-mr20">¥{{item.marketprice}}</text> <text class="rui-fs20" wx:if="{{showMoney}}">Highest earning<text class="rui-color17">{{item.predictAmt}}</text>element</text> </view> </view> </view> <view slot="right" class="rui-cancel-collect" data-item="{{item}}" catchtap="cancelCollect">Cancel collection</view> </rui-swipe-cell> </view>
7. Summary
- For performance reasons, try not to use it if it can not be used;
- Unless the global state is used by multiple pages and multiple components at the same time, the business logic is complex and easy to be confused, and the use of the global state is convenient for management, do not set it to the global state;
- In subscription optimization, try to execute only updated subscriptions;
- setData modifies the view layer data. Try to modify only the partially changed part, not all.