当前位置: 首页 > news >正文

【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 主要问题

  1. 代码重复

    • 每个组件都需要编写类似的数据获取逻辑
    • 错误处理和加载状态管理代码重复
    • 生命周期处理代码重复
  2. 可维护性差

    • 错误处理分散在各处
    • 难以统一修改数据获取逻辑
    • 依赖关系不明确
  3. 功能受限

    • 不支持多接口协同
    • 缺乏数据共享机制
    • 配置项不够灵活

3. 改造方案

3.1 设计思路

组件
useOperatorData
数据获取
状态管理
生命周期
单接口
多接口
并行
串行
错误处理
加载状态
数据共享
自动清理
依赖追踪

3.2 核心改进

OperatorCallback
+fn: Function
+options: OperatorCallbackOptions
OperatorCallbackOptions
+immediate: boolean
+deps: Function
+onError: Function
+order: number
+mode: string
+retryTimes: number
+share: boolean
UseOperatorData
-operatorTypeStore: Store
-emitter: Emitter
-sharedData: Ref
+getParams()
+onOperatorChange()
+watchWithDeps()

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>

这个基础版本展示了:

  1. 简化的数据流程

    • 单个数据获取函数 getData
    • 使用 Promise.all 并发请求
    • 清晰的错误处理
  2. Hook 的基本用法

    • 使用 watchWithDeps 替代原有的 onMounted 和手动监听
    • 配置 immediate: true 实现自动加载
    • 统一的错误处理
  3. 状态管理

    • 加载状态 isLoading
    • 空状态处理 isEmpty
    • 图表数据管理
  4. 最佳实践

    • 使用 getParams 统一处理运营商参数
    • 集中的错误处理
    • 清晰的代码组织

这个基础版本保留了核心功能,同时大大简化了代码结构,更容易理解和维护。它展示了如何使用 useOperatorData hook 的主要功能,以及如何处理常见的数据加载和状态管理场景。

5. 改造效果

5.1 代码量对比

30% 25% 20% 25% 组件代码量减少 数据获取代码 状态管理代码 生命周期代码 其他业务代码

5.2 提升效果

  1. 开发效率

    • 样板代码减少 60%
    • 配置项集中管理
    • 类型提示完善
  2. 可维护性

    • 错误处理统一
    • 生命周期自动管理
    • 依赖关系清晰
  3. 功能增强

    • 支持多接口协作
    • 支持数据共享
    • 支持更多配置项

6. 设计模式分析

6.1 使用的设计模式

  1. 组合模式

    • 将多个接口调用组合成一个整体
    • 统一处理接口的执行顺序和依赖关系
  2. 观察者模式

    • 监听运营商变化
    • 自动触发数据刷新
  3. 策略模式

    • 支持不同的执行模式(并行/串行)
    • 支持不同的错误处理策略

6.2 架构设计

数据层
数据转换
API调用
数据缓存
状态管理层
共享数据
运营商状态
错误状态
Hook层
watchWithDeps
useOperatorData
getParams
组件层
Hook层
状态管理层
数据层

7. 最佳实践

  1. 选择合适的调用方式

    • 简单场景使用函数式调用
    • 复杂场景使用配置式调用
  2. 错误处理策略

    • 全局错误处理用于通用逻辑
    • 局部错误处理用于特殊逻辑
  3. 性能优化

    • 合理使用 immediate 选项
    • 适当配置重试次数
    • 注意依赖项的设置

8. 未来展望

  1. 功能扩展

    • 支持取消请求
    • 添加缓存机制
    • 支持更多执行模式
  2. 性能优化

    • 添加防抖/节流
    • 优化依赖收集
    • 减少不必要的请求
  3. 开发体验

    • 提供更多辅助函数
    • 完善开发文档
    • 添加更多使用示例

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>;

相关文章:

  • [数据库之九] 数据库索引之顺序索引
  • ​IP 风险画像如何实现对恶意 IP 的有效拦截?
  • NetBox:运维利器,网络与数据中心管理平台
  • 使用 Vue CLI 和 vuedraggable 实现拖拽排序功能
  • 优艾智合CEO张朝辉荣膺U45杰出青年企业家
  • OG-HFYOLO:当梯度方向引导遇见异构特征融合,变形表格分割难题迎刃而解
  • 【Python】os模块
  • Soft Mask(软遮罩)技术
  • MySQL的information_schema在SQL注入中的关键作用与防御策略
  • 后端返回文件流,前端展示图片
  • AI视觉质检的落地困境与突破路径
  • 架构进阶:精读麦肯锡-_电力公司业务能力架构设计规划咨询项目【附全文阅读】
  • 雪兽云资产助力“星耀汇聚”提升业务效率
  • J2 WebScarab 安装指南详细步骤与配置方法
  • Python入门(二)
  • 【Python os模块完全指南】从基础到高效文件操作
  • PyQt5 实现自定义滑块,效果还不错
  • 【信息系统项目管理师】法律法规与标准规范——历年考题(2024年-2020年)
  • ts 工具类型
  • 【PostgreSQL数据分析实战:从数据清洗到可视化全流程】7.1 主流可视化工具对比(Tableau/Matplotlib/Python库)
  • 古埃及展进入百天倒计时,闭幕前168小时不闭馆
  • 我驻苏丹使馆建议在苏中国公民尽快撤离
  • 深圳下调公积金利率,209万纯公积金贷款总利息减少9.94万
  • 复旦设立新文科发展基金,校友曹国伟、王长田联合捐赠1亿元
  • 自然资源部印发地理信息数据分类分级指南
  • 碧桂园服务:拟向杨惠妍全资持有的公司提供10亿元贷款,借款将转借给碧桂园用作保交楼