【useOperatorData Hook 改造实践】
useOperatorData Hook 改造实践
1. 背景
在我们的大屏项目中,运营商数据是一个核心的业务概念。几乎所有业务模块都需要根据当前选择的运营商来获取对应的数据。这就要求我们有一个统一的、可靠的方式来处理运营商相关的数据获取和状态变更。
1.1 原有实现
最初的实现是一个简单的 useOperatorData
hook:
// 原始版本
export function useOperatorData() {const operatorTypeStore = useOperatorTypeStore();const { emitter } = useEmitt();const getParams = (extraParams?: FetchParams) => ({operatorIdSet: operatorTypeStore.getCurrentOperatorIdSet(),...(extraParams || {})});const onOperatorChange = (callback: () => void) => {const stopWatch = watch(() => operatorTypeStore.currentOperator.value, callback);emitter.on('change-operator-type', callback);onBeforeUnmount(() => {stopWatch();emitter.off('change-operator-type');});};return { getParams, onOperatorChange };
}
1.2 使用场景
// 组件中的使用
const { getParams, onOperatorChange } = useOperatorData();// 获取数据
const getData = async () => {const params = getParams({ category: '3' });const data = await api.fetchData(params);
};// 监听变化
onOperatorChange(() => {getData();
});onMounted(() => {getData();
});
2. 痛点分析
mindmaproot((运营商数据痛点))数据处理重复的样板代码错误处理不统一加载状态管理分散生命周期手动管理监听器需要显式调用清理组件卸载容易遗漏数据流转多接口调用复杂数据共享困难依赖关系不清晰开发体验类型提示不完善配置项分散代码复用性差
2.1 主要问题
-
代码重复
- 每个组件都需要编写类似的数据获取逻辑
- 错误处理和加载状态管理代码重复
- 生命周期处理代码重复
-
可维护性差
- 错误处理分散在各处
- 难以统一修改数据获取逻辑
- 依赖关系不明确
-
功能受限
- 不支持多接口协同
- 缺乏数据共享机制
- 配置项不够灵活
3. 改造方案
3.1 设计思路
3.2 核心改进
3.3 改造后代码
export interface OperatorCallbackOptions {immediate?: boolean;deps?: () => any[];onError?: (error: any) => void;order?: number;mode?: 'parallel' | 'serial';retryTimes?: number;share?: boolean;
}export function useOperatorData() {// ... 核心实现见上文 ...const watchWithDeps = (callbacks: OperatorCallback | OperatorCallback[] | (() => Promise<any> | any),globalOptions: OperatorGlobalOptions = {}) => {// 支持多种调用方式// 支持数据共享// 支持错误重试// 支持执行顺序控制};return {getParams,onOperatorChange,watchWithDeps};
}
4. 使用示例
4.1 基础用法
const { watchWithDeps } = useOperatorData();// 简单场景
watchWithDeps(getData, {immediate: true
});
4.2 多接口协作
watchWithDeps([{fn: async () => {const baseData = await fetchBaseData();return baseData; // 共享结果},options: {order: 1,share: true}},{fn: async () => {const details = await fetchDetails();return details;},options: {order: 2,mode: 'serial'}}
], {immediate: true,onSuccess: (results) => {console.log('所有数据加载完成');}
});
4.3 基础使用案例
下面是一个基于实际业务组件的基础使用示例:
// user-analysis.vue
<template><div class="user-analysis-page"><BaseTitle title="用户分析"></BaseTitle><div class="blue-opacity-box user-analysis-box" v-loading="isLoading"><!-- 柱状图 --><div class="user-analysis-eCharts-barLine"><div class="unit" v-show="!isEmpty[0]">单位:个</div><EcResize class="ec-resize-box" :option="chartsBarLineOption" v-show="!isEmpty[0]" /><NoData v-show="isEmpty[0]" /></div><!-- 饼图 --><div class="user-analysis-eCharts-line"><div class="blue-barLine-title">订单占比</div><EcResizeclass="ec-resize-box":option="chartsPieOption"v-show="!isEmpty[1]"/><NoData v-show="isEmpty[1]" /></div></div></div>
</template><script setup lang="ts">
import { ref } from 'vue';
import { useOperatorData } from '@/hooks/useOperatorData';
import Api from '../api';// 状态定义
const isLoading = ref(false);
const isEmpty = ref([false, false]);
const chartsBarLineOption = ref();
const chartsPieOption = ref();// 使用 hook
const { watchWithDeps, getParams } = useOperatorData();// 数据获取和处理
const getData = async () => {isLoading.value = true;try {const params = getParams({ category: '3' });// 并发请求多个接口const [barData, pieData] = await Promise.all([Api.chargingOperationIncome(params),Api.getChargeTimeAndAmount(params)]);// 处理数据if (barData) {chartsBarLineOption.value = processBarChartData(barData);isEmpty.value[0] = !barData.length;}if (pieData) {chartsPieOption.value = processPieChartData(pieData);isEmpty.value[1] = !pieData.length;}} catch (error) {console.error('Error:', error);isEmpty.value = [true, true];} finally {isLoading.value = false;}
};// 使用 watchWithDeps 监听运营商变化并自动刷新数据
watchWithDeps(getData, {immediate: true, // 组件挂载时立即执行onError: (error) => {console.error('数据加载失败:', error);isEmpty.value = [true, true];}
});// 数据处理函数
const processBarChartData = (data) => {// 处理柱状图数据...return {// 图表配置...};
};const processPieChartData = (data) => {// 处理饼图数据...return {// 图表配置...};
};
</script><style lang="less" scoped>
.user-analysis-page {// 基础样式...
}
</style>
这个基础版本展示了:
-
简化的数据流程
- 单个数据获取函数
getData
- 使用
Promise.all
并发请求 - 清晰的错误处理
- 单个数据获取函数
-
Hook 的基本用法
- 使用
watchWithDeps
替代原有的onMounted
和手动监听 - 配置
immediate: true
实现自动加载 - 统一的错误处理
- 使用
-
状态管理
- 加载状态
isLoading
- 空状态处理
isEmpty
- 图表数据管理
- 加载状态
-
最佳实践
- 使用
getParams
统一处理运营商参数 - 集中的错误处理
- 清晰的代码组织
- 使用
这个基础版本保留了核心功能,同时大大简化了代码结构,更容易理解和维护。它展示了如何使用 useOperatorData
hook 的主要功能,以及如何处理常见的数据加载和状态管理场景。
5. 改造效果
5.1 代码量对比
5.2 提升效果
-
开发效率
- 样板代码减少 60%
- 配置项集中管理
- 类型提示完善
-
可维护性
- 错误处理统一
- 生命周期自动管理
- 依赖关系清晰
-
功能增强
- 支持多接口协作
- 支持数据共享
- 支持更多配置项
6. 设计模式分析
6.1 使用的设计模式
-
组合模式
- 将多个接口调用组合成一个整体
- 统一处理接口的执行顺序和依赖关系
-
观察者模式
- 监听运营商变化
- 自动触发数据刷新
-
策略模式
- 支持不同的执行模式(并行/串行)
- 支持不同的错误处理策略
6.2 架构设计
7. 最佳实践
-
选择合适的调用方式
- 简单场景使用函数式调用
- 复杂场景使用配置式调用
-
错误处理策略
- 全局错误处理用于通用逻辑
- 局部错误处理用于特殊逻辑
-
性能优化
- 合理使用 immediate 选项
- 适当配置重试次数
- 注意依赖项的设置
8. 未来展望
-
功能扩展
- 支持取消请求
- 添加缓存机制
- 支持更多执行模式
-
性能优化
- 添加防抖/节流
- 优化依赖收集
- 减少不必要的请求
-
开发体验
- 提供更多辅助函数
- 完善开发文档
- 添加更多使用示例
9. 总结
这次改造是一次非常成功的实践,不仅提升了代码质量和开发效率,还为未来的功能扩展打下了良好的基础。通过合理的抽象和设计模式的运用,我们创造了一个更加强大和灵活的工具。
希望这次改造的经验能够帮助到其他开发者,在面对类似场景时能够提供一些思路和参考。
附录:改造后源码
import { ref, onMounted, onBeforeUnmount, watch } from 'vue';
import { useOperatorTypeStore } from '@/store/operator-type';
import { useEmitt } from '@some/vue3-hooks';interface FetchParams {[key: string]: any;
}// 单个回调的配置选项
export interface OperatorCallbackOptions {immediate?: boolean; // 是否立即执行deps?: () => any[]; // 额外的依赖项onError?: (error: any) => void; // 错误处理order?: number; // 执行顺序,数字越小越先执行mode?: 'parallel' | 'serial'; // 并行或串行执行模式retryTimes?: number; // 失败重试次数share?: boolean; // 是否共享结果给其他回调使用
}// 全局配置选项
export interface OperatorGlobalOptions extends OperatorCallbackOptions {onSuccess?: (results: any[]) => void; // 所有回调成功后的处理
}// 回调函数的完整配置
export interface OperatorCallback {fn: () => Promise<any> | any; // 回调函数options?: OperatorCallbackOptions; // 回调特定的配置
}export function useOperatorData() {const operatorTypeStore = useOperatorTypeStore();const { emitter } = useEmitt();const isFirstRun = ref(true);const sharedData = ref<Record<string, any>>({}); // 用于存储共享数据// 获取带运营商参数的完整参数const getParams = (extraParams?: FetchParams) => {return {operatorIdSet: operatorTypeStore.getCurrentOperatorIdSet(),...(extraParams || {})};};// 监听运营商变更事件const onOperatorChange = (callback: () => void) => {// 监听运营商变化const stopWatch = watch(() => operatorTypeStore.currentOperator.value, callback);// 监听运营商变更事件emitter.on('change-operator-type', callback);// 组件卸载时自动清理onBeforeUnmount(() => {stopWatch();emitter.off('change-operator-type');});};// 执行单个回调const runSingleCallback = async (callback: OperatorCallback | (() => Promise<any> | any),globalOptions?: OperatorGlobalOptions) => {const maxRetries = globalOptions?.retryTimes || 0;let retryCount = 0;const execute = async (): Promise<any> => {try {// 处理旧版本的直接函数调用const result = typeof callback === 'function' ? await callback() : await callback.fn();if (globalOptions?.share && typeof callback !== 'function') {// 如果需要共享结果,存储到 sharedDataconst callbackIndex = String(callback.options?.order || 0);sharedData.value[callbackIndex] = result;}return result;} catch (error) {if (retryCount < maxRetries) {retryCount++;return execute(); // 重试}if (typeof callback === 'function') {globalOptions?.onError?.(error);} else {(callback.options?.onError || globalOptions?.onError)?.(error);}throw error;}};return execute();};/*** 增强版的运营商数据监听* @param callbacks 单个回调或回调数组* @param globalOptions 全局配置选项*/const watchWithDeps = (callbacks: OperatorCallback | OperatorCallback[] | (() => Promise<any> | any),globalOptions: OperatorGlobalOptions = {}) => {// 处理旧版本的直接函数调用if (typeof callbacks === 'function') {const callbackFn = callbacks;callbacks = {fn: callbackFn,options: globalOptions};}const callbackArray = Array.isArray(callbacks) ? callbacks : [callbacks];// 按 order 排序const sortedCallbacks = callbackArray.sort((a, b) =>((a as OperatorCallback).options?.order || 0) -((b as OperatorCallback).options?.order || 0));// 执行所有回调const runCallbacks = async () => {try {let results: any[] = [];if (globalOptions.mode === 'parallel') {// 并行执行results = await Promise.all(sortedCallbacks.map((callback) => runSingleCallback(callback, globalOptions)));} else {// 串行执行for (const callback of sortedCallbacks) {const result = await runSingleCallback(callback, globalOptions);results.push(result);}}globalOptions.onSuccess?.(results);return results;} catch (error) {globalOptions.onError?.(error);}};// 监听运营商变化和其他依赖const deps = () => [operatorTypeStore.currentOperator.value, ...(globalOptions.deps?.() || [])];// 立即执行一次(如果需要)if (globalOptions.immediate) {runCallbacks();}// 设置监听const stopWatch = watch(deps,() => {runCallbacks();},{ deep: true });// 监听运营商变更事件const handleOperatorChange = () => {runCallbacks();};emitter.on('change-operator-type', handleOperatorChange);// 组件卸载时自动清理onBeforeUnmount(() => {stopWatch();emitter.off('change-operator-type', handleOperatorChange);});return {runCallbacks,sharedData,currentOperator: operatorTypeStore.currentOperator};};return {getParams,onOperatorChange,watchWithDeps,currentOperator: operatorTypeStore.currentOperator};
}// 导出类型
export type OperatorData = ReturnType<typeof useOperatorData>;