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拆分指导,而非强制性的规范要求。

相关推荐
heartmoonq几秒前
个人对于sign的理解
前端
ZzMemory几秒前
告别移动端适配烦恼!pxToViewport 凭什么取代 lib-flexible?
前端·css·面试
Running_C4 分钟前
从「救命稻草」到「甜蜜的负担」:我对 TypeScript 的爱恨情仇
前端·typescript
蒟蒻小袁30 分钟前
力扣面试150题--阶乘后的零,Pow(x,n)直线上最多的点
leetcode·面试·哈希算法
前端搬运侠35 分钟前
📝从零到一封装 React 表格:基于 antd Table 实现多条件搜索 + 动态列配置,代码可直接复用
前端
歪歪10037 分钟前
Vue原理与高级开发技巧详解
开发语言·前端·javascript·vue.js·前端框架·集成学习
zabr37 分钟前
我让AI一把撸了个算命网站,结果它比我还懂玄学
前端·aigc·ai编程
xianxin_39 分钟前
CSS Fonts(字体)
前端
用户25191624271139 分钟前
Canvas之画图板
前端·javascript·canvas
快起来别睡了1 小时前
前端设计模式:让代码更优雅的“万能钥匙”
前端·设计模式