Hooks拆分最佳实践指南

Hooks拆分最佳实践指南

1. 核心理念

实用主义原则:拆分是为了解决问题,不是为了遵循规范。只有当Hook确实难以维护、测试或复用时,才考虑拆分。

2. 拆分时机判断

2.1 何时需要拆分

基于维护难度
javascript 复制代码
 // ❌ 难以维护的Hook
 export const useContractManagement = () => {
   // 200+ 行代码,包含:
   // - 合同列表逻辑
   // - 合同表单逻辑  
   // - 合同弹窗逻辑
   // - 合同搜索逻辑
   // - 合同导出逻辑
   // - 权限检查逻辑
   // 修改一个功能需要理解整个文件
 }
 ​
 // ✅ 拆分后的Hook
 export const useContractList = () => {
   // 只关注列表相关逻辑
 }
 ​
 export const useContractForm = () => {
   // 只关注表单相关逻辑
 }
 ​
 export const useContractDialog = () => {
   // 只关注弹窗相关逻辑
 }
基于复用需求
javascript 复制代码
 // ❌ 难以复用的Hook
 export const useContractManagement = () => {
   // 组件A只需要列表功能,但被迫引入所有逻辑
   // 组件B只需要表单功能,但被迫引入所有逻辑
 }
 ​
 // ✅ 可组合的Hook
 // 组件A
 const { contractList, loading, fetchData } = useContractList()
 ​
 // 组件B  
 const { formData, validateForm, submitForm } = useContractForm()
基于测试复杂度
javascript 复制代码
 // ❌ 难以测试的Hook
 export const useContractManagement = () => {
   // 依赖太多外部服务
   // 状态太多,难以mock
   // 测试用例需要覆盖太多场景
 }
 ​
 // ✅ 易于测试的Hook
 export const useContractList = () => {
   // 依赖少,状态清晰
   // 容易mock和测试
 }

2.2 何时不需要拆分

简单逻辑保持内联
csharp 复制代码
 // ✅ 简单逻辑,保持内联
 const isVisible = ref(false)
 const handleToggle = () => {
   isVisible.value = !isVisible.value
 }
 ​
 // ❌ 过度拆分
 const { isVisible, handleToggle } = useToggle()
业务逻辑紧密相关
ini 复制代码
 // ✅ 业务逻辑紧密相关,保持在一起
 export const useUserProfile = () => {
   const userInfo = ref(null)
   const loading = ref(false)
   
   const fetchUserInfo = async () => {
     loading.value = true
     userInfo.value = await api.getUserInfo()
     loading.value = false
   }
   
   const updateUserInfo = async (data) => {
     loading.value = true
     await api.updateUserInfo(data)
     await fetchUserInfo() // 更新后重新获取
     loading.value = false
   }
   
   return { userInfo, loading, fetchUserInfo, updateUserInfo }
 }

3. 拆分策略

3.1 按功能职责拆分

基础功能拆分
ini 复制代码
 // 表格基础功能
 export const useTable = () => {
   const loading = ref(false)
   const data = ref([])
   const total = ref(0)
   
   const fetchData = async (params) => {
     loading.value = true
     try {
       const response = await api.getList(params)
       data.value = response.data
       total.value = response.total
     } finally {
       loading.value = false
     }
   }
   
   return { loading, data, total, fetchData }
 }
 ​
 // 分页功能
 export const usePagination = () => {
   const currentPage = ref(1)
   const pageSize = ref(10)
   
   const handlePageChange = (page) => {
     currentPage.value = page
   }
   
   const handlePageSizeChange = (size) => {
     pageSize.value = size
     currentPage.value = 1
   }
   
   return { currentPage, pageSize, handlePageChange, handlePageSizeChange }
 }
 ​
 // 搜索功能
 export const useSearch = () => {
   const searchForm = ref({})
   
   const handleSearch = () => {
     // 触发搜索逻辑
   }
   
   const handleReset = () => {
     searchForm.value = {}
   }
   
   return { searchForm, handleSearch, handleReset }
 }
业务功能拆分
javascript 复制代码
 // 合同业务逻辑
 export const useContract = () => {
   const { loading, data, fetchData } = useTable()
   const { currentPage, pageSize, handlePageChange } = usePagination()
   const { searchForm, handleSearch } = useSearch()
   
   const createContract = async (contractData) => {
     await api.createContract(contractData)
     await fetchData({ currentPage: currentPage.value, pageSize: pageSize.value })
   }
   
   const updateContract = async (id, contractData) => {
     await api.updateContract(id, contractData)
     await fetchData({ currentPage: currentPage.value, pageSize: pageSize.value })
   }
   
   const deleteContract = async (id) => {
     await api.deleteContract(id)
     await fetchData({ currentPage: currentPage.value, pageSize: pageSize.value })
   }
   
   return {
     loading,
     data,
     currentPage,
     pageSize,
     searchForm,
     fetchData,
     handlePageChange,
     handleSearch,
     createContract,
     updateContract,
     deleteContract
   }
 }
 ​
 // 合同弹窗逻辑
 export const useContractDialog = () => {
   const dialogVisible = ref(false)
   const dialogTitle = ref('')
   const dialogLoading = ref(false)
   const currentContract = ref(null)
   
   const openCreateDialog = () => {
     dialogVisible.value = true
     dialogTitle.value = '新建合同'
     currentContract.value = null
   }
   
   const openEditDialog = (contract) => {
     dialogVisible.value = true
     dialogTitle.value = '编辑合同'
     currentContract.value = { ...contract }
   }
   
   const closeDialog = () => {
     dialogVisible.value = false
     currentContract.value = null
   }
   
   return {
     dialogVisible,
     dialogTitle,
     dialogLoading,
     currentContract,
     openCreateDialog,
     openEditDialog,
     closeDialog
   }
 }

3.2 按生命周期拆分

页面初始化逻辑
javascript 复制代码
 // 页面初始化
 export const usePageInit = () => {
   const isInitialized = ref(false)
   const initError = ref(null)
   
   const initPage = async () => {
     try {
       await Promise.all([
         loadUserInfo(),
         loadPermissions(),
         loadInitialData()
       ])
       isInitialized.value = true
     } catch (error) {
       initError.value = error
     }
   }
   
   return { isInitialized, initError, initPage }
 }
 ​
 // 数据加载
 export const useDataLoad = () => {
   const loadUserInfo = async () => {
     // 加载用户信息
   }
   
   const loadPermissions = async () => {
     // 加载权限信息
   }
   
   const loadInitialData = async () => {
     // 加载初始数据
   }
   
   return { loadUserInfo, loadPermissions, loadInitialData }
 }

3.3 按状态管理拆分

状态分组
ini 复制代码
 // 表单状态管理
 export const useFormState = () => {
   const formData = ref({})
   const formRules = ref({})
   const formRef = ref()
   const formLoading = ref(false)
   
   const validateForm = async () => {
     if (!formRef.value) return false
     return await formRef.value.validate()
   }
   
   const resetForm = () => {
     if (formRef.value) {
       formRef.value.resetFields()
     }
     formData.value = {}
   }
   
   return { formData, formRules, formRef, formLoading, validateForm, resetForm }
 }
 ​
 // 表格状态管理
 export const useTableState = () => {
   const tableData = ref([])
   const selectedRows = ref([])
   const sortBy = ref('')
   const sortOrder = ref('asc')
   
   const handleSelectionChange = (rows) => {
     selectedRows.value = rows
   }
   
   const handleSortChange = (column, order) => {
     sortBy.value = column
     sortOrder.value = order
   }
   
   return {
     tableData,
     selectedRows,
     sortBy,
     sortOrder,
     handleSelectionChange,
     handleSortChange
   }
 }

4. 实际项目示例

4.1 合同管理页面Hook拆分

javascript 复制代码
 // views/contract-management/utils/hooks/
 // ├── useContractList.ts      // 合同列表逻辑
 // ├── useContractForm.ts      // 合同表单逻辑
 // ├── useContractDialog.ts    // 合同弹窗逻辑
 // ├── useContractSearch.ts    // 合同搜索逻辑
 // ├── useContractExport.ts    // 合同导出逻辑
 // └── index.ts               // 统一导出
 ​
 // index.ts
 export { useContractList } from './useContractList'
 export { useContractForm } from './useContractForm'
 export { useContractDialog } from './useContractDialog'
 export { useContractSearch } from './useContractSearch'
 export { useContractExport } from './useContractExport'
 ​
 // 在组件中使用
 import { 
   useContractList, 
   useContractForm, 
   useContractDialog 
 } from './utils/hooks'
 ​
 // 组件实现
 export default defineComponent({
   setup() {
     const { 
       contractList, 
       loading, 
       fetchData, 
       handlePageChange 
     } = useContractList()
     
     const { 
       formData, 
       validateForm, 
       submitForm 
     } = useContractForm()
     
     const { 
       dialogVisible, 
       openCreateDialog, 
       closeDialog 
     } = useContractDialog()
     
     return {
       contractList,
       loading,
       formData,
       dialogVisible,
       fetchData,
       handlePageChange,
       validateForm,
       submitForm,
       openCreateDialog,
       closeDialog
     }
   }
 })

4.2 通用Hook组合使用

javascript 复制代码
 // src/hooks/useTable.ts - 通用表格Hook
 export const useTable = (apiFunction) => {
   const loading = ref(false)
   const data = ref([])
   const total = ref(0)
   
   const fetchData = async (params = {}) => {
     loading.value = true
     try {
       const response = await apiFunction(params)
       data.value = response.data
       total.value = response.total
     } finally {
       loading.value = false
     }
   }
   
   return { loading, data, total, fetchData }
 }
 ​
 // 业务Hook组合通用Hook
 export const useContractList = () => {
   const { loading, data, total, fetchData } = useTable(api.getContractList)
   const { currentPage, pageSize, handlePageChange } = usePagination()
   
   // 业务特定逻辑
   const handleContractAction = async (action, id) => {
     switch (action) {
       case 'delete':
         await api.deleteContract(id)
         break
       case 'approve':
         await api.approveContract(id)
         break
     }
     await fetchData({ currentPage: currentPage.value, pageSize: pageSize.value })
   }
   
   return {
     loading,
     data,
     total,
     currentPage,
     pageSize,
     fetchData,
     handlePageChange,
     handleContractAction
   }
 }

5. 拆分检查清单

5.1 拆分前检查

  • 这个Hook是否真的很难维护?
  • 这个Hook是否被多个地方使用但只需要部分功能?
  • 这个Hook是否违反了单一职责原则?
  • 拆分后是否真的能提高代码质量?
  • 拆分后是否便于测试?

5.2 拆分后检查

  • 依赖关系是否清晰?
  • 命名是否语义化?
  • 是否避免了循环依赖?
  • 是否便于复用?
  • 是否便于维护?

6. 反模式示例

6.1 过度拆分

scss 复制代码
 // ❌ 过度拆分 - 每个状态都单独Hook
 const { loading } = useLoading()
 const { data } = useData()
 const { total } = useTotal()
 const { currentPage } = useCurrentPage()
 const { pageSize } = usePageSize()
 ​
 // ✅ 合理拆分 - 相关状态组合
 const { loading, data, total, currentPage, pageSize } = useTable()

6.2 功能混杂

scss 复制代码
 // ❌ 功能混杂 - 一个Hook包含多种职责
 export const useContract = () => {
   // 表格逻辑
   const tableData = ref([])
   const loading = ref(false)
   
   // 表单逻辑
   const formData = ref({})
   const formRules = ref({})
   
   // 弹窗逻辑
   const dialogVisible = ref(false)
   
   // 搜索逻辑
   const searchForm = ref({})
   
   return { /* 返回所有状态和方法 */ }
 }
 ​
 // ✅ 职责分离
 const { tableData, loading } = useContractList()
 const { formData, formRules } = useContractForm()
 const { dialogVisible } = useContractDialog()
 const { searchForm } = useContractSearch()

7. 命名规范

7.1 Hook命名原则

scss 复制代码
 // ✅ 好的命名示例
 useTablePagination()     // 表格分页
 useFormValidation()      // 表单验证
 useDialogControl()       // 弹窗控制
 useDataFetch()          // 数据获取
 useUserPermission()     // 用户权限
 ​
 // ❌ 避免的命名
 useTable()              // 太宽泛
 useData()               // 太宽泛
 useHook()               // 无意义
 useContractTable()      // 混合了业务和UI

7.2 文件组织规范

scss 复制代码
 // ✅ 页面级Hook组织
 views/contract-management/
 ├── utils/
 │   └── hooks/
 │       ├── useContractList.ts
 │       ├── useContractForm.ts
 │       ├── useContractDialog.ts
 │       └── index.ts
 ​
 // ✅ 全局Hook组织
 src/hooks/
 ├── useTable.ts
 ├── usePagination.ts
 ├── useSearch.ts
 ├── useDialog.ts
 └── index.ts

8. 总结

Hook拆分应该遵循以下原则:

  1. 基于实际问题拆分,而不是预设的标准
  2. 关注业务边界和可维护性,而不是代码行数
  3. 团队共识优先,而不是个人偏好
  4. 渐进式改进,而不是一次性重构
  5. 保持简单,避免过度工程化

记住:拆分是为了解决问题,不是为了遵循规范。只有当Hook确实难以维护、测试或复用时,才考虑拆分。


本文档基于实际开发经验总结,旨在提供实用的Hook拆分指导,而非强制性的规范要求。

相关推荐
再吃一根胡萝卜6 小时前
08 · 前端:Vue3 + SSE 流式与执行链路可视化
面试
再吃一根胡萝卜6 小时前
03 · 后端:FastAPI 与分层架构
面试
mldong7 小时前
你的 Vue3 项目也能有钉钉同款审批流设计器:npm 装包,10 分钟画出第一条审批流
前端·vue.js
2分钟速写快排8 小时前
什么是 RAG?如何用 RAG 实现一个用户记忆?
前端·后端·ai编程
passerby60618 小时前
如何自己造一个时间处理库
前端·javascript·github
走到天涯海角9 小时前
react里面的长列表渲染优化
前端·react.js·前端框架
小羊没烦恼!9 小时前
Hello Web API系列教程——Web API与国际化
java·服务器·前端·javascript·php
北岛贰10 小时前
迷茫焦虑期,我做了一个带支付带官网的 AI 聊天虚拟恋人 App
前端·人工智能·后端
Interview Aid11210 小时前
Walmart Global Tech SDE 三轮面经|基础、并发、压力面
面试·职场和发展
mayaairi11 小时前
Vue2 组件通讯(三):全局事件总线、PubSub、插槽与组件实例属性
前端·javascript·vue.js