Vuex_Pinia处理基础数据

原创
2023/02/14 11:04
阅读数 427

1 应用场景

我们在前端开发时, 经常会遇到这样一个场景.<br />销售订单/销售退货单等单据的编辑, 实际库存的条件查询, 都有需要商品基础数据的地方, 如select选择框.<br />如果每次遇到这样的场景, 都去重新查询一遍商品数据, 不论是前端还是后端, 都会造成性能浪费. 数据量比较大导致查询时间比较长, 用户体验也不会好.

一个比较好的设计方式是, 使用vuex或pinia这样的全局状态存储框架, 作为商品数据的缓存.<br />但在实际使用的时候, 还是有很多逻辑要点需要注意.

2 逻辑要点

还是以商品数据为例, 在实际开发和用户使用过程中, 可能会遇到如下几个问题:

  1. 页面中存在多个组件, 每个组件都有查询商品基础数据的需求. 多个组件同时发送请求就就会导致请求重复问题. 因而要求:
    • 多个组件同时有获取商品数据的请求时, 对后端仍是只发送一次请求
    • 在请求结束后, 自动对多个组件请求产生回应
    • 在读取时, 有的场合需要提供loading变量来优化显示效果
  2. 缓存数据的更新时机问题. 这里就要涉及到取舍了, 即何时更新缓存. 这个问题比较复杂, 我们留在最后再说.

3 代码示例

这里使用Pinia作为全局数据存储的框架. 同时用typeScript来编写, 这个对于理解代码更有帮助.

Pinia封装工具类方法

import { watch } from 'vue';

/**
 * 通用数据类型
 * 后端接口发送数据回来, 均会以该类型为统一的接收类型. 其中, data是查询的实际数据, 如商品列表
 */
interface ActionResult<T = any> {
	success: boolean;
	message: string;
	data: T;
}

/**
 * Pinia基础数据的参数配置. 
 * 以商品基础数据查询为例:
 * listName是store的商品列表数据存储的名称
 * loadingName是商品列表数据的loading变量. 如果此值为true, 就表示已经有后端请求接口在发送了, 现在等待现成的后端接口数据就可以, 不必再重新发送请求.
 * loadListFunc是商品列表的api请求方法. 注: 此处的api请求方法实际是封装过的, 已经将网络请求的元数据中直接取出了data数据作为ActionResult
 * defaultCondition是基础数据查询接口的默认查询条件. 商品列表这个场景用不到, 但有些场景会用到, 如根据type查询分类数据.
 */
interface PiniaBasicConfig<T> {
	listName: string;
	loadingName: string;
	loadListFunc: (any) => Promise<ActionResult<T[]>>;
	defaultCondition?: any;
}

/**
 * 读取列表数据的方法
 * forceUpdate表示强制读取
 */
interface LoadListFunc<T> {
	({ forceUpdate: boolean }?): Promise<T[]>;
}

/**
 * 非典型的通用数据查询接口.
 * 用以提供ActionResult里的数据, 不是基础列表类型的情况. 如果为了理解方便, 可以先不管这里.
 */
interface PiniaPostConfig<T = any> {
	resultName: string;
	loadingName: string;
	postRequestFunc: (any) => Promise<ActionResult<T>>;
	defaultCondition?: any;
	// 如果获取对象为空时, 是否给定一个默认对象替代
	null2Default?: any;
}

interface PostRequestFunc<T = any> {
	({ forceUpdate: boolean }?): Promise<T>;
}

// pinia的包装工具类
class PiniaWrapUtil {
	/**
	 * 获取计算基础集合的方法
	 * 注:该方法需要在actions的方法体中使用,否则无法正确获取this!!!
	 * 使用方法参考PiniaUtil.calcLoadBasic<T>(this,{...})(condition)
	 * @param currentStore 实际代表的是store里的this
	 * @param config 各类配置
	 */
	static calcLoadBasic<T>(currentStore: object, config: PiniaBasicConfig<T>): LoadListFunc<T> {
		function loadList(
			{ forceUpdate = false }: { forceUpdate: boolean } = { forceUpdate: false }
		): Promise<T[]> {
			if (currentStore[config.loadingName]) {
        // loading为true, 表示已经有后端请求在处理. 等待其结果即可
				return new Promise<T[]>(resolve => {
					//try to use vue watch
					watch(
						() => currentStore[config.loadingName],
						val => {
							if (!val) {
								return resolve(currentStore[config.listName]);
							}
						}
					);
				});
			} else {
        // 第一次或强制的发送后端接口请求.
				return new Promise(resolve => {
					if (forceUpdate || currentStore[config.listName].length === 0) {
						currentStore[config.loadingName] = true;
						config
							.loadListFunc(config.defaultCondition)
							.then(res => {
								if (res.success) {
									currentStore[config.listName] = res.data;
								}
								return resolve(res.data);
							})
							.finally(() => {
								currentStore[config.loadingName] = false;
							});
					} else {
						return resolve(currentStore[config.listName]);
					}
				});
			}
		}
		return loadList;
	}

	/**
	 * 发送post请求的方法
	 * @param currentStore
	 * @param config
	 */
	static calcPostRequest<T = any>(
		currentStore: object,
		config: PiniaPostConfig<T>
	): PostRequestFunc<T> {
		function postRequest(
			{ forceUpdate = false }: { forceUpdate: boolean } = { forceUpdate: false }
		): Promise<T> {
			if (currentStore[config.loadingName]) {
				return new Promise<any>(resolve => {
					//try to use vue watch
					watch(
						() => currentStore[config.loadingName],
						val => {
							if (!val) {
								return resolve(currentStore[config.resultName]);
							}
						}
					);
				});
			} else {
				return new Promise(resolve => {
					if (forceUpdate || !currentStore[config.resultName]) {
						currentStore[config.loadingName] = true;
						config
							.postRequestFunc(config.defaultCondition)
							.then(res => {
								if (res.success) {
									if (!res.data && config.null2Default) {
										currentStore[config.resultName] = config.null2Default;
									} else {
										currentStore[config.resultName] = res.data;
									}
								}
								return resolve(res.data);
							})
							.finally(() => {
								currentStore[config.loadingName] = false;
							});
					} else {
						return resolve(currentStore[config.resultName]);
					}
				});
			}
		}
		return postRequest;
	}
}

export { PiniaWrapUtil };

Pinia的store方法

商品store比较复杂, 这里改用staff职员举例.

import { defineStore } from 'pinia';
import { queryAllStaff, Staff } from '@/api/basicSettingModule/trafficDataModule/staffManagement';
import { CommonBasic } from '@/api/basicSettingModule/commonBasic';
import { PiniaWrapUtil } from '@/utils/storage/piniaWrapUtil';

interface StaffStore {
	staffList: Staff[];
	staffLoading: boolean;
}

function convertStaff2Common(staff: Staff): CommonBasic {
	return {
		id: staff.id,
		code: staff.code,
		name: staff.name,
		categoryId: staff.departmentId
	};
}

const useStaffStore = defineStore('staff', {
	state: (): StaffStore => ({
		staffList: [],
		staffLoading: false
	}),
	getters: {
		commonList(state): CommonBasic[] {
			return state.staffList.map(m => convertStaff2Common(m));
		}
	},
	actions: {
		loadList(condition: { forceUpdate: boolean } = { forceUpdate: false }): Promise<Staff[]> {
      // 这里是调用PiniaWrapUtil封装工具方法的调用方式
			return PiniaWrapUtil.calcLoadBasic<Staff>(this, {
				listName: 'staffList',
				loadingName: 'staffLoading',
				loadListFunc: queryAllStaff
			})(condition);
		}
	}
});

export { useStaffStore };

调用store示例

const staffStore = useStaffStore();
await staffStore.loadList();
commonBasicList.splice(0, commonBasicList.length, ...staffStore.commonList);

4 缓存更新时机的几个建议

以上的封装方法中, 解决了第一类问题(也可理解为前端全局数据的并发问题). <br />第二类问题(缓存数据更新问题)最简单的方式, 是用户发现数据不正确后, 手动刷新浏览器更新全局数据. 这可以满足绝大多数基础数据场合, 因为大多数基础数据的变更并不会很频繁.

但若一定要从代码层面解决这个问题. 有以下思路可供借鉴:

  1. 最严格的一种方式, 可以保证前端全局缓存与后端数据必然一致: 每次都去查询后端接口, 但查询的不是全部的基础数据, 而是基础数据的版本标记. 并据此判断是否需要更新.
    1. 版本标记: 用以表示基础数据的版本. 相同的版本的基础数据, 必然相同.
    2. 一个可考虑的版本标记方案是: 基础数据的最近修改时间+基础数据数量. 以商品为例, 如果从后端查询到的最新商品修改时间和商品数量都与前端缓存中一致, 则说明前端基础数据一致, 不必再拉取商品数据了.
    3. 版本标记法, 缺点是开发起来有些麻烦, 需要多考虑一个后端接口. 适用于基础数据分组情况下频繁变更的场合.
  2. 不算太严格, 但也能满足基本需要的方法: 设定一个定时器, 如30分钟, 每隔一段时间重新查询一遍基础数据.
    1. 适用于用户持续使用账套的情况.
    2. 缺点除了存在一定的前后端不统一情况外, 前端需要额外的封装.

就笔者经验来说, 多数基础数据的变更都不是均匀的, 所以多数情况下, 也不需要特意考虑缓存数据的更新问题.<br />即使前端数据不是最新的, 只要在保存时, 后端检查统一性即可.

end

展开阅读全文
加载中
点击引领话题📣 发布并加入讨论🔥
打赏
0 评论
0 收藏
0
分享
返回顶部
顶部