从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践

从组件点击事件到业务统一入口:一次前端操作链的完整解耦实践

1.业务需求和收益点

业务需求:

每个项目可能支持多种操作动作包括处理模型中暂停 / 恢复 / 重跑 / 删除"的地方相关的接口,每个动作按钮既要展示图标,也要执行对应的业务接口。各个组件内部都写业务导致维护成本高。

将项目操作动作配置化、组件化,让 UI 不再参与业务逻辑,构建统一的、可扩展的动作体系

收益点:

  • 收益点 :UI 与业务逻辑彻底解耦
  • 动作按钮可配置、可扩展
  • 图标、文案、逻辑全部集中管理,易维护

2.文件结构和调用链

文件结构:

arduino 复制代码
ProjectListView/
  ├─ components/
  │    └─ ProjectCard.vue
  │
  ├─ settings/
  │    ├─ hooks/
  │    │    └─ useProjectActions.ts   //统一业务处理中心
  │    │
  │    └─ actionIcon.ts //动作图标配置模块
  │
  └─ index.vue

调用链:

2.原始代码部分示例

原始代码部分示例(4 个操作函数):

ProjectListView/index.vue

typescript 复制代码
// 处理暂停图标点击(切换显示三个操作图标)
const handlePauseClick = async (project: ProjectListItem) => {
  try {
    const response = await ProjectService.pauseTrain({ projectId: project.projectId })
    if (response.code === 1) {
      ElMessage.success('建图已暂停')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '暂停失败')
    }
  } catch (error: unknown) {
    console.error('暂停训练失败:', error)
    const errorMessage = error instanceof Error ? error.message : '暂停训练失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理恢复图标点击(恢复为只显示暂停图标的状态)
const handleResumeClick = async (projectId: number) => {
  try {
    const response = await ProjectService.resumeTrain({ projectId })
    if (response.code === 1) {
      ElMessage.success('项目已恢复建图')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '恢复失败')
    }
  } catch (error: unknown) {
    console.error('恢复训练失败:', error)
    const errorMessage = error instanceof Error ? error.message : '恢复训练失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理重跑图标点击
const handleRerunClick = async (projectId: number) => {
  try {
    const response = await ProjectService.restartTrain({ projectId })
    if (response.code === 1) {
      ElMessage.success('项目已开始重跑')
      await getProjectList()
    } else {
      ElMessage.error(response.message || '重跑失败')
    }
  } catch (error: unknown) {
    console.error('重跑项目失败:', error)
    const errorMessage = error instanceof Error ? error.message : '重跑项目失败'
    ElMessage.error(errorMessage)
  }
}
​
// 处理删除图标点击
const handleDeleteClick = async (projectId: number) => {
  try {
    await ElMessageBox.confirm(删除后,项目数据不可恢复, '确认删除该项目?', {
      confirmButtonText: '确定',
      cancelButtonText: '取消',
      closeOnClickModal: false,
      customClass: 'tenant-confirm-messagebox',
    })
​
    try {
      const response = await ProjectService.deleteProject({ projectId })
      if (response.code === 1) {
        ElMessage.success('删除成功')
        await getProjectList()
      } else {
        ElMessage.error(response.message || '删除失败')
      }
    } catch (error: unknown) {
      console.error('删除项目失败:', error)
      const errorMessage = error instanceof Error ? error.message : '删除项目失败'
      ElMessage.error(errorMessage)
    }
  } 
}

简单概述一下这个代码:

参数名叫 project,他的类型设置为 ProjectListItem(写在 types 文件夹中)

const response = await ProjectService.pauseTrain({ projectId: project.projectId })

{ projectId: project.projectId }是对象字面量中的[键:值]写法,传过去的数据是{projectId:123456}

const response = await ProjectService.deleteProject({ projectId })

{ projectId }是一种简写的写法,{ projectId: projectId}可以直接写成{ projectId }

API 层中的

csharp 复制代码
 static async pauseTrain(params: PauseTrainParams): Promise<PauseTrainResponse> {
    const { projectId } = params

const { projectId } = params 为解构写法,如果 params 中存在一个叫 projectId 的值,拿出来变成一个新的同名变量,然后再使用

3.统一动作处理逻辑的封装与使用

在 ProjectListView/settings/hooks,创建 useProjectActions.ts,抽出逻辑

3.1 封装后的使用方式

先看封装后,怎么用,在看怎么封装

1.接入

javascript 复制代码
import {
  getStateUIConfig,
  PROJECT_TYPE_MENU_LIST,
  PROJECT_TYPE_MAP,
  createActionIconConfig,
  useProjectActions,
  useUrlSync,
} from './settings/index'

对应的 hook 统一有 index 管理,所以这么接入

2.将 hook 所需要的函数(刷新列表函数),传给他

php 复制代码
// 项目操作处理
const { handleAction } = useProjectActions({
  refreshProjectList: getProjectList,
})

这行代码本身是一个完全独立的"初始化"步骤,把getProjectList 传给hook,跟后面给 hook 传 id 啥的不在一条线上。

3.将handleAction 传给动作配置项

scss 复制代码
// 操作图标配置
const actionIconConfig = createActionIconConfig(handleAction)

这里的逻辑是,文件 A 中导入了 B,把 B 传给了 C,在 C 中调用 B

4.在模版中使用该配置

ruby 复制代码
<ProjectCard
            :action-icon-config="actionIconConfig"
          />

在父组件中把这个配置单传给子组件,子组件来进行具体的使用。

3.2 封装实现细节

actionIcon.ts 文件:

typescript 复制代码
import type { Component } from 'vue'
import type { ProjectListItem } from '@/types/project'
import PauseIcon from '@/assets/svgs/HomePauseIcon.vue'
import ResumeIcon from '@/assets/svgs/ResumeIcon.vue'
import DeleteIcon from '@/assets/svgs/DeleteIcon.vue'
import RerunIcon from '@/assets/svgs/RerunIcon.vue'
​
//字符串枚举
export type ActionIconType = 'pause' | 'resume' | 'delete' | 'rerun'
​
//动作-图标映射
export const ACTION_ICON_COMPONENTS = {
  pause: PauseIcon,
  resume: ResumeIcon,
  delete: DeleteIcon,
  rerun: RerunIcon,
} as const
​
//一个操作图标,需要的所有配置
export interface ActionIconConfig {
  component: Component
  tooltip: string
  handler: (project: ProjectListItem) => void
}
​
// 统一的操作处理函数的类型
export type HandleAction = (actionType: ActionIconType, project: ProjectListItem) => Promise<void>
​
​
//操作图标映射-统一处理
export function createActionIconConfig(
  handleAction: HandleAction,
): Record<ActionIconType, ActionIconConfig> {
  return {
    pause: {
      component: ACTION_ICON_COMPONENTS.pause,
      tooltip: '暂停',
      handler: (project) => handleAction('pause', project),
    },
    resume: {
      component: ACTION_ICON_COMPONENTS.resume,
      tooltip: '恢复',
      handler: (project) => handleAction('resume', project),
    },
    delete: {
      component: ACTION_ICON_COMPONENTS.delete,
      tooltip: '删除',
      handler: (project) => handleAction('delete', project),
    },
    rerun: {
      component: ACTION_ICON_COMPONENTS.rerun,
      tooltip: '重跑',
      handler: (project) => handleAction('rerun', project),
    },
  }
}
​
​
//判断是否为操作图标类型
export function isActionIconType(actionType: string): actionType is ActionIconType {
  return ['pause', 'resume', 'delete', 'rerun'].includes(actionType)
}

动作-图标映射为什么要加 as const?

为什么要加as const?

不加 as const 的话,TS 会把它推成一个比较宽泛的类型,比如:

less 复制代码
{
  pause: Component
  resume: Component
  delete: Component
  rerun: Component
}

加上 as const,TS 会更精确地认为:value 是这些具体组件本身,而不是随便一个 Component

=> void 的意思?

=> void:这个函数 不关心返回值 / 不需要返回任何东西

export type HandleAction = (actionType: ActionIconType, project: ProjectListItem) => Promise的含义是什么?

"有这么一种函数:

  • 第一个参数叫 actionType,只能是 'pause' | 'resume' | 'delete' | 'rerun'
  • 第二个参数叫 project,是一个 ProjectListItem 对象
  • 这个函数是异步的,返回 Promise

这个函数createActionIconConfig 的输入和产出是什么?

javascript 复制代码
export function createActionIconConfig(
  handleAction: HandleAction,
): Record<ActionIconType, ActionIconConfig>
  • 输入就是 handleAction,对应的类型HandleAction,这是从 index.vue 中传过来的
  • 产出: Record<ActionIconType, ActionIconConfig>

Record<K, V> 可以理解成:

一个对象:

  • key 的类型是 K
  • value 的类型是 V
  • K = ActionIconType → 'pause' | 'resume' | 'delete' | 'rerun'
  • V = ActionIconConfig → 每个动作对应一条配置,结构是:
typescript 复制代码
{
  component: Component
  tooltip: string
  handler: (project: ProjectListItem) => void
}

所以返回的东西大致长这样:

yaml 复制代码
{
  pause:  { component: ..., tooltip: '暂停', handler: (...) => {} },
  resume: { component: ..., tooltip: '恢复', handler: (...) => {} },
  delete: { ... },
  rerun:  { ... },
}

也就是说整个一个返回就是配置单对象

useProjectActions.ts(自定义 hook) 文件:

typescript 复制代码
import { ElMessage, ElMessageBox } from 'element-plus'
import { ProjectService } from '@/services/project'
import type { ProjectListItem } from '@/types/project'
import type { HandleAction } from '../actionIcon'
​
//项目操作处理函数的参数
export interface UseProjectActionsOptions {
  //刷新项目列表的函数
  refreshProjectList: () => Promise<void>
}
​
//项目操作处理
export function useProjectActions(options: UseProjectActionsOptions): {
  handleAction: HandleAction
} {
  const { refreshProjectList } = options
​
  const handlePause = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.pauseTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('建图已暂停')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '暂停失败')
      }
    } catch (error: unknown) {
      console.error('暂停训练失败:', error)
      const errorMessage = error instanceof Error ? error.message : '暂停训练失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleResume = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.resumeTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('项目已恢复建图')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '恢复失败')
      }
    } catch (error: unknown) {
      console.error('恢复训练失败:', error)
      const errorMessage = error instanceof Error ? error.message : '恢复训练失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleRerun = async (project: ProjectListItem) => {
    try {
      const response = await ProjectService.restartTrain({ projectId: project.projectId })
      if (response.code === 1) {
        ElMessage.success('项目已开始重跑')
        await refreshProjectList()
      } else {
        ElMessage.error(response.message || '重跑失败')
      }
    } catch (error: unknown) {
      console.error('重跑项目失败:', error)
      const errorMessage = error instanceof Error ? error.message : '重跑项目失败'
      ElMessage.error(errorMessage)
    }
  }
​
  const handleDelete = async (project: ProjectListItem) => {
    try {
      await ElMessageBox.confirm(`删除后,项目数据不可恢复`, '确认删除该项目?', {
        confirmButtonText: '确定',
        cancelButtonText: '取消',
        closeOnClickModal: false,
        customClass: 'tenant-confirm-messagebox',
      })
​
      try {
        const response = await ProjectService.deleteProject({ projectId: project.projectId })
        if (response.code === 1) {
          ElMessage.success('删除成功')
          await refreshProjectList()
        } else {
          ElMessage.error(response.message || '删除失败')
        }
      } catch (error: unknown) {
        console.error('删除项目失败:', error)
        const errorMessage = error instanceof Error ? error.message : '删除项目失败'
        ElMessage.error(errorMessage)
      }
    } 
  }
​
  //统一入口函数
  const handleAction: HandleAction = async (actionType, project) => {
    switch (actionType) {
      case 'pause':
        await handlePause(project)
        break
      case 'resume':
        await handleResume(project)
        break
      case 'rerun':
        await handleRerun(project)
        break
      case 'delete':
        await handleDelete(project)
        break
      default:
        console.warn(`未知的操作类型: ${actionType}`)
    }
  }
​
  return {
    handleAction,
  }
}
​

其中

typescript 复制代码
export interface UseProjectActionsOptions {
  refreshProjectList: () => Promise<void>
}

useProjectActions 接收的那个配置对象里,必须有一个 refreshProjectList 函数

arduino 复制代码
export function useProjectActions(options: UseProjectActionsOptions): {
  handleAction: HandleAction
} 

定义了输入输出,输入一个刷新函数,输出一个处理函数

然后就是定义项目处理的操作

然后就是通过将这个函数导出,被父组件传递给createActionIconConfig 操作映射

统一入口函数的参数就是createActionIconConfig 传过去的

相关推荐
import_random2 小时前
[python]miniconda(安装)
前端
云梦谭2 小时前
AI 生成的FreeSWITCH 呼出流程深度分析freeswitch-1.10.12.-release
java·前端·php
秃了才能变得更强2 小时前
React Native小技巧
前端
一只爱吃糖的小羊2 小时前
React 19 vs Vue 3:深度对比与选型指南
前端·vue.js·react.js
前端老宋Running2 小时前
Vue 3 的“降维打击”:Composition API 是如何让 Mixin 成为历史文物的?
前端·javascript·vue.js
Pluto_CRown2 小时前
H5 开发的各类小知识点
前端·javascript
Pluto_CRown2 小时前
上下文存储【下】
前端·javascript
AAA阿giao2 小时前
JavaScript 中基于原型和原型链的继承方式详解
前端·javascript·面试
用户600071819102 小时前
【翻译】如何在Vue中使用Suspense处理异步渲染?
前端