Vue3 组合式 API 最佳实践:hooks 封装、业务逻辑抽离、代码复用方案

📅 2026/8/4 8:28:48
Vue3 组合式 API 最佳实践:hooks 封装、业务逻辑抽离、代码复用方案
Hi我是前端人类学Vue3 的组合式 APIComposition API带来了代码组织方式的根本性变革。它真正解决了 Vue2 时代逻辑复用的两大顽疾——命名冲突和来源不透明让组件代码从“按选项类型分散”转向“按功能关注点聚合”。本文从实战出发系统梳理hooks封装的核心原则、业务逻辑抽离方案及代码复用的完整策略。文章目录一、为什么需要组合式 API—— 从 Options API 的痛点说起二、hooks 封装的核心原则2.1 命名规范与基本结构2. 2 三大设计原则2.3 响应式状态共享的陷阱与解法三、hooks 封装实战三个典型场景场景一数据请求 HookuseFetch场景二表单管理 HookuseForm场景三验证码倒计时 HookuseCountDown四、业务逻辑抽离的分层策略五、ref vs reactive在 composable 中如何选择六、与 Pinia 的边界什么时候用 hooks什么时候用 Pinia一、为什么需要组合式 API—— 从 Options API 的痛点说起在 Options API 中一个功能的逻辑被拆散到data、methods、computed、watch等不同选项中。当组件代码超过 300 行时理解某个功能需要在多个选项之间反复跳转维护成本急剧上升。// Options API 写法同一功能的代码分散在四个选项中exportdefault{data(){return{searchKeyword:,// 搜索功能searchResults:[]// 搜索功能}},methods:{asyncdoSearch(){/* 搜索功能 */}// 搜索功能},watch:{searchKeyword:doSearch// 搜索功能但写在 watch 里},computed:{hasResults(){/* 搜索功能 */}// 搜索功能但写在 computed 里}}这种分散让代码难以维护也使得逻辑复用必须依赖 Mixins——但 Mixins 会带来命名冲突多个 Mixins 定义同名属性和来源不透明难以追踪逻辑来自哪个 Mixin的问题。组合式 API 的解法很简单把同一个功能的所有逻辑状态、计算属性、方法、生命周期钩子封装在一个函数里。二、hooks 封装的核心原则2.1 命名规范与基本结构自定义 Hook 遵循use前缀的命名约定这既是社区共识也便于 IDE 和工具链识别。// hooks/useCounter.tsimport{ref}fromvueexportfunctionuseCounter(initialValue0){constcountref(initialValue)functionincrement(){count.value}functiondecrement(){count.value--}return{count,increment,decrement}}2. 2 三大设计原则一个好的 composable 应该具备三个特征原则说明单一职责每个 composable 只负责一个明确的功能领域自包含内部响应式数据和副作用自成闭环不依赖外部环境可测试输入和输出通过参数和返回值明确表达过度的拆分会导致 composable 之间的依赖关系复杂化适度控制粒度比追求极致拆分更重要。2.3 响应式状态共享的陷阱与解法这是 hooks 封装中最容易被忽略的问题。每次调用 composable 函数都会创建全新的响应式状态因此多个组件调用同一个 composable 时拿到的不是同一份状态。// ❌ 错误每次调用都创建新的 countexportfunctionuseStore(){constcountref(0)return{count}}// ✅ 正确将状态定义在函数外部实现共享constcountref(0)exportfunctionuseStore(){return{count}}如果需要在多个组件间共享同一份状态将响应式数据定义在 composable 函数外部即可。如果状态更复杂可以考虑配合 Pinia 使用。三、hooks 封装实战三个典型场景场景一数据请求 HookuseFetch数据请求是最常见的逻辑复用场景。一个设计良好的useFetch应该包含数据、加载状态、错误状态并可选的请求取消能力。// hooks/useFetch.tsimport{ref}fromvueexportfunctionuseFetchT(url:string){constdatarefT|null(null)constloadingref(false)consterrorrefError|null(null)constfetchDataasync(){loading.valuetrueerror.valuenulltry{constresponseawaitfetch(url)if(!response.ok)thrownewError(HTTP${response.status})data.valueawaitresponse.json()}catch(err){error.valueerrinstanceofError?err:newError(未知错误)}finally{loading.valuefalse}}return{data,loading,error,fetchData}}组件中使用script setupimport{useFetch}from/hooks/useFetchconst{data,loading,error,fetchData}useFetch(/api/users)/script进阶优化在并发场景下需要考虑请求竞态处理——使用AbortController取消过期请求避免先发后至的请求覆盖最新数据。场景二表单管理 HookuseForm表单 Hook 封装了表单状态、验证规则和提交逻辑避免在每个表单组件中重复编写验证代码。// hooks/useForm.tsimport{reactive,ref}fromvueexportfunctionuseFormTextendsRecordstring,any(initialValues:T,validate:(values:T)PartialRecordkeyofT,string){constvaluesreactiveT({...initialValues})consterrorsrefPartialRecordkeyofT,string({})consthandleSubmit(callback:(values:T)void){constvalidationErrorsvalidate(values)errors.valuevalidationErrorsif(Object.keys(validationErrors).length0){callback(values)}}return{values,errors,handleSubmit}}场景三验证码倒计时 HookuseCountDown封装具有副作用的 UI 逻辑自动管理定时器的创建与销毁。// hooks/useCountDown.tsimport{ref,onUnmounted}fromvueexportfunctionuseCountDown(){constcountref(0)lettimer:ReturnTypetypeofsetInterval|nullnullconststart(seconds:number,onFinish?:()void){if(count.value0)returncount.valueseconds timersetInterval((){count.value--if(count.value0){clearInterval(timer!)timernullonFinish?.()}},1000)}onUnmounted((){if(timer){clearInterval(timer)timernull}})return{count,start}}四、业务逻辑抽离的分层策略对于大型组件可以引入分层设计思想层级职责示例数据层API 调用、数据缓存、状态管理useUserApi、useCache交互层表单校验、UI 状态切换、用户操作响应useForm、useModal业务规则层跨组件的业务逻辑封装usePermission、useWorkflow组件本身退化为薄薄的组装层只负责将各层 composable 的输出按模板结构排列。五、ref vs reactive在 composable 中如何选择这是高频困惑在 composable 返回值设计中尤为关键对比维度refreactive适用类型基本类型 对象需整体替换时对象、数组访问方式.valueJS中直接属性访问解构响应性配合toRefs可保持直接解构会丢失整体替换✅ 可以❌ 不能直接重新赋值推荐策略composable 返回值优先暴露ref而非reactive对象——因为解构reactive对象会丢失响应性而模板中对ref的解构不会有问题。六、与 Pinia 的边界什么时候用 hooks什么时候用 Piniahooks 和 Pinia 都可以封装状态和逻辑但适用场景不同场景推荐方案单个组件内或父子组件间共享逻辑自定义 Hook跨多个不相关组件共享状态Pinia需要持久化存储localStorage两者皆可Pinia 有插件支持页面级状态路由参数派生Hook配合useRoute应用级全局状态用户信息、主题Pinia如果一个 hooks 需要在多个组件间共享同一份状态把状态定义在 hooks 函数外部即可实现轻量级共享不一定需要引入 Pinia。组合式 API 的最佳实践可以概括为按功能组织代码将同一功能的状态、计算、方法、生命周期封装在一个 composable 中而非分散在 options 中遵循三大设计原则单一职责、自包含、可测试注意状态共享边界多个组件调用同一 composable 时默认拿到的是独立状态如需共享将状态定义在函数外部返回值优先用 ref避免解构 reactive 导致响应性丢失分层抽离数据层、交互层、业务规则层各司其职组件只做组装善用生命周期清理在onUnmounted中清除事件监听和定时器避免内存泄漏从 Options API 到组合式 API 的迁移不需要一步到位。推荐的策略是在新组件中全面使用组合式 API 建立规范对老组件在每次修改时逐步重构——修改哪部分功能就把哪部分逻辑抽取为 composable。