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拆分应该遵循以下原则:
- 基于实际问题拆分,而不是预设的标准
- 关注业务边界和可维护性,而不是代码行数
- 团队共识优先,而不是个人偏好
- 渐进式改进,而不是一次性重构
- 保持简单,避免过度工程化
记住:拆分是为了解决问题,不是为了遵循规范。只有当Hook确实难以维护、测试或复用时,才考虑拆分。
本文档基于实际开发经验总结,旨在提供实用的Hook拆分指导,而非强制性的规范要求。