mominger / blog

Tech blog
45 stars 3 forks source link

WeChat Mini Program State Management Solution Design #45

Open mominger opened 3 months ago

mominger commented 3 months ago

The upper part is the English version, and the lower part is the Chinese version, with the same content. If there are any wrong, or you have anything hard to understand, pls feel free to let me know.many thx.

1 Overview

This document discusses a suitable state management solution for WeChat Mini Programs. After comparing various options, the Proxy solution is recommended, and a detailed analysis of the Proxy solution is provided.

2 Comparison of Different Solutions

Solution Advantages Disadvantages
Redux Clear state management Large library size, increases the app size
Complex configuration
Suitable for medium to large projects
MobX Simple reactive programming Large library size, increases app size
Suitable for medium and large projects, especially those needing a lot of reactive updates
Global Data Built-in support in WeChat Mini Programs
Easy to use
Does not support asynchronous updates
Event Bus Loose coupling between components, flexible event-based communication Complex state management, hard to maintain
Suitable for small projects with complex event flows
Proxy Automatic response to state changes
Simple code implementation
A large number of proxy objects may affect performance
Suitable for small applications

Since most WeChat Mini Programs are small applications, the Proxy solution is recommended. However, if there are too many proxy objects, consider other solutions like MobX.

3 Proxy Solution Design

3.1 Core Source Code Reference
class Store {
  instance = null;
  callBackList = {};

  constructor() {
    this.createInstance();
  }

  createInstance() {
    if (!this.instance) {
      // init dataSource
      const dataSource = {};
      this.instance = new Proxy(dataSource, {
        set: (target, key, value, receiver) => {
          const result = Reflect.set(target, key, value, receiver);
          if (this.callBackList[key]) {
            // execute every callback for the corresponding field
            Object.values(this.callBackList[key]).flat().forEach(callback => callback(value));
          }
          return result;
        }
      });
    }
    return this.instance;
  }

  get(key) {
    return this.instance[key];
  }

  set(key, value) {
    this.instance[key] = value;
  }

  subscribe(key, callback, componentInstance) {
    const id = componentInstance.is;
    if (!this.callBackList[key]) {
      this.callBackList[key] = {};
    }
    if (!this.callBackList[key][id]) {
      this.callBackList[key][id] = [];
    }
    this.callBackList[key][id].push(callback);
  }

  unsubscribe(key, componentInstance) {
    const id = componentInstance.is;
    if (this.callBackList[key] && this.callBackList[key][id]) {
      delete this.callBackList[key][id];
    }
  }
}

export default new Store();

Note: The above solution uses the Proxy API, which requires the WeChat Mini Program version to be >= 2.11.0.

If you need to support lower versions of the Mini Program, please use the source code below.

class Store {
  callBackList = {};  // save callbacks 
  data = {};          // save states 

  constructor() {}

  get(key) {
    return this.data[key];
  }
  set(key, value) {
    this.data[key] = value;
    if (this.callBackList[key]) {
      const callbacks = Object.values(this.callBackList[key]).reduce(
        (acc, cur) => acc.concat(cur),
        []
      );
      callbacks.forEach(callback => callback(value));
    }
  }
  subscribe(key, callback, componentInstance) {
    // mark every component
    const id = componentInstance.is; 
    if (!this.callBackList[key]) {
      this.callBackList[key] = {};
    }
    if (!this.callBackList[key][id]) {
      this.callBackList[key][id] = [];
    }
    this.callBackList[key][id].push(callback);
  }

  unsubscribe(key, componentInstance) {
    const id = componentInstance.is;
    if (this.callBackList[key] && this.callBackList[key][id]) {
      delete this.callBackList[key][id];
    }
  }
}

export default new Store();
3.2 Unified Data Source
3.2.1 Data Source Structure
let callBackList = {
  "userInfo": {
     "Page/Component 1 ID": callback,
     "Page/Component 2 ID": callback
  }
}
3.3 Synchronous Updates
// Set value synchronously
Store.set('userInfo', newUserInfo);
// Update synchronously
this.setData({
    userInfo: Store.get('userInfo')
});
3.4 Asynchronous Updates
// Asynchronous update
// Subscribe to changes in userInfo
Store.subscribe('userInfo', (newUserInfo) => {
    this.setData({
    userInfo: newUserInfo
    });
}, this);
// Unsubscribe when the page is unloaded
onUnload() {
    Store.unsubscribe('userInfo', this);
}
3.5 Modularization

Consider creating state modules, especially for information frequently used throughout the project, such as user profile information or i18n. For example, user profile information:

class ProfileStore {
  static get() {
    if (!this.initialized) {
      this.init();
    }
    return Store.get(USER_PROFILE);
  }

  static set(locale) {
    Store.instance[USER_PROFILE] = locale;
  }

  static subscribe(callback, componentInstance) {
    Store.subscribe(USER_PROFILE, callback, componentInstance);
  }

  static unsubscribe(componentInstance) {
    Store.unsubscribe(USER_PROFILE, componentInstance);
  }
}

export default ProfileStore;

Usage is simpler:

// Subscribe
ProfileStore.subscribe(this.updateProfile, this)

// Unsubscribe
ProfileStore.unsubscribe(this)

// Set value
ProfileStore.set(profile)

// Get value
ProfileStore.get()

The following is the Chinese version, the same content as above

1 概述

本文主要针对微信小程序探讨一个合适的状态管理方案,在经过各方案对比后,推荐使用Proxy方案,并对Proxy方案进行详细的分析。

2 不同方案对比

方案 优势 劣势
Redux 状态管理清晰 库较大,增加小程序体积
配置繁琐
适合中大型项目
MobX 简洁的响应式编程 库较大,增加项目体积
适合中型项目以上,尤其需要大量响应式更新的场景
Global Data 微信小程序内置支持
容易使用
不支持异步更新
Event Bus - 组件间松耦合,通过事件灵活传递信息 - 状态管理复杂,难以维护
适合小型项目,需要复杂事件流的场景
Proxy - 自动化响应状态变化
代码实现简洁
- 大量代理对象可能影响性能
- 适合小型应用

基于大部分微信小程序都是小型应用,推荐采用 Proxy方案。 但如果代理的对象过多,则要考虑其他方案如Mbox等。

3 Proxy 方案设计

3.1 核心源码参考
class Store {
  instance = null;
  callBackList = {};

  constructor() {
    this.createInstance();
  }

  createInstance() {
    if (!this.instance) {
      // init dataSource
      const dataSource = {};
      this.instance = new Proxy(dataSource, {
        set: (target, key, value, receiver) => {
          const result = Reflect.set(target, key, value, receiver);
          if (this.callBackList[key]) {
            // execute every callback for the corresponding field
            Object.values(this.callBackList[key]).flat().forEach(callback => callback(value));
          }
          return result;
        }
      });
    }
    return this.instance;
  }

  get(key) {
    return this.instance[key];
  }

  set(key, value) {
    this.instance[key] = value;
  }

  subscribe(key, callback, componentInstance) {
    const id = componentInstance.is;
    if (!this.callBackList[key]) {
      this.callBackList[key] = {};
    }
    if (!this.callBackList[key][id]) {
      this.callBackList[key][id] = [];
    }
    this.callBackList[key][id].push(callback);
  }

  unsubscribe(key, componentInstance) {
    const id = componentInstance.is;
    if (this.callBackList[key] && this.callBackList[key][id]) {
      delete this.callBackList[key][id];
    }
  }
}

export default new Store();

注意,上面的方案用到Proxy API, 它要求所使用的小程序的版本>=2.11.0 如果要兼容小程序低版本,请采用下面的源码

class Store {
  callBackList = {};  // save callbacks 
  data = {};          // save states 

  constructor() {}

  get(key) {
    return this.data[key];
  }
  set(key, value) {
    this.data[key] = value;
    if (this.callBackList[key]) {
      const callbacks = Object.values(this.callBackList[key]).reduce(
        (acc, cur) => acc.concat(cur),
        []
      );
      callbacks.forEach(callback => callback(value));
    }
  }
  subscribe(key, callback, componentInstance) {
    // mark every component
    const id = componentInstance.is; 
    if (!this.callBackList[key]) {
      this.callBackList[key] = {};
    }
    if (!this.callBackList[key][id]) {
      this.callBackList[key][id] = [];
    }
    this.callBackList[key][id].push(callback);
  }

  unsubscribe(key, componentInstance) {
    const id = componentInstance.is;
    if (this.callBackList[key] && this.callBackList[key][id]) {
      delete this.callBackList[key][id];
    }
  }
}

export default new Store();
3.2 统一数据源
3.2.1 数据源结构
let callBackList = {
  "userInfo": {
    "页面/组件1 ID": callback,
    "页面/组件2 ID": callback
  }
}
3.3 同步更新
// 同步设值
Store.set('userInfo', newUserInfo);
// 同步更新
this.setData({
    userInfo: Store.get('userInfo')
});
3.4 异步更新
// 异步更新
// 订阅 userInfo 的变化
Store.subscribe('userInfo', (newUserInfo) => {
    this.setData({
    userInfo: newUserInfo
    });
}, this);
// 页面卸载时取消订阅
onUnload() {
    Store.unsubscribe('userInfo', this);
}
3.5 模块化

可以考虑做一些状态模块封装,尤其对整个项目都会用到的用户信息,或国际化等 比如用户信息

class ProfileStore {
  static get() {
    if (!this.initialized) {
      this.init();
    }
    return Store.get(USER_PROFILE);
  }

  static set(locale) {
    Store.instance[USER_PROFILE] = locale;
  }

  static subscribe(callback, componentInstance) {
    Store.subscribe(USER_PROFILE, callback, componentInstance);
  }

  static unsubscribe(componentInstance) {
    Store.unsubscribe(USER_PROFILE, componentInstance);
  }
}

export default ProfileStore;

使用更简洁

  // 订阅
  ProfileStore.subscribe(this.updateProfile,this)

  // 取消订阅
  ProfileStore.unsubscribe(this)

  // 设值
  ProfileStore.set(profile)

  // 取值
  ProfileStore.get()