封装规范实战:命名 / 输入输出 / 复用边界 + 避坑|Vue 组件与模板规范篇)
【Vue3 组合式函数】【前端开发实战】从命名、输入输出到复用边界彻底搞懂组合式函数的最佳封装写法避开响应式丢失、副作用未清理等高频坑 文章目录一、前言为什么要单独讲「组合式函数」二、组合式函数到底是什么三、命名规范一看就知道用途3.1 函数命名use 动词/名词3.2 返回值命名结构清晰、语义明确3.3 参数命名一看就懂用法四、输入输出设计边界要清晰4.1 输入尽量用「配置对象」而不是「一长串参数」4.2 输出只暴露必要内容4.3 完整示例useRequest 的输入输出设计五、复用边界什么时候该抽、抽成什么5.1 判断标准3 次原则5.2 常见边界划分5.3 反例把业务写死在 Hook 里5.4 完整示例useCountdown 的边界六、与 Vue 组件、模板的配合规范6.1 组件只负责「组合」和「视图」6.2 模板里避免复杂逻辑6.3 组件 props 与 Hooks 的配合七、常见坑与避坑指南7.1 忘记处理响应式7.2 生命周期和副作用没清理7.3 把组合式函数当工具函数用八、小结一套可直接照搬的检查清单 系列模块导航同学们好我是 Eugene尤金一名多年中后台前端开发工程师。Eugene 发音 /juːˈdʒiːn/大家怎么顺口怎么叫就好很多前端开发者都会遇到一个瓶颈代码能跑但不够规范功能能实现但维护起来特别痛苦一个人写没问题一到团队协作就各种混乱、踩坑、返工。想写出干净、优雅、可维护的专业代码靠的不是天赋而是体系化的规范 真实实战经验。这一系列《前端规范实战》我会用大白话 真实业务场景不讲玄学、不堆理论只分享能直接落地的规范、标准与避坑指南。帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。一、前言为什么要单独讲「组合式函数」如果你已经能写 Vue 组件但总觉得抽逻辑时不知道往哪儿放自己写的useXxx别人难复用复用逻辑时容易和业务强耦合多半是组合式函数Composition API / Hooks的边界没理清。这篇文章不讲底层原理只讲日常写代码该怎么选、为什么这么选、坑会出在哪儿。目标是帮你形成一套可落地的封装规范并在业务里持续复用。⬆ 返回目录二、组合式函数到底是什么简单说把可复用的响应式逻辑抽到一个函数里这个函数就叫组合式函数。它必须以use开头命名约定俗成只处理逻辑不渲染模板可以被多个组件重复调用组合式函数 ≠ 组件它是“逻辑单元”不是“视图单元”。⬆ 返回目录三、命名规范一看就知道用途3.1 函数命名use 动词/名词// ✅ 好的命名动词表示行为useToggle()// 切换状态useRequest()// 发请求useLocalStorage()// 读写本地存储useDebounce()// 防抖// ✅ 好的命名名词表示能力useCountdown()// 倒计时usePagination()// 分页useTableSelection()// 表格选中// ❌ 不好的命名含义模糊useData()// 什么数据干什么用useHandle()// 处理什么useLogic()// 逻辑太泛原则从名字就能猜出功能和职责。⬆ 返回目录3.2 返回值命名结构清晰、语义明确// ✅ 推荐对象解构名字和用途一一对应functionuseCounter(initial0){constcountref(initial)constincrement()count.valueconstdecrement()count.value--return{count,increment,decrement}}// 使用时const { count, increment, decrement } useCounter()// ✅ 也可以数组返回适合顺序固定、数量少的场景functionuseToggle(initialfalse){conststateref(initial)consttoggle(){state.value!state.value}return[state,toggle]}// 使用时const [isVisible, toggleVisible] useToggle()尽量避免返回值里既有数据又有方法名字却很抽象如data、methods别人很难猜到怎么用。⬆ 返回目录3.3 参数命名一看就懂用法// ✅ 好参数有默认值、含义清晰functionuseRequest(url,options{}){const{immediatetrue,methodGET}options// ...}// ✅ 好用对象收拢可选参数functionusePagination({pageSize10,total0,currentPage1}{}){// ...}// ❌ 差参数多且无默认值调用方容易传错functionuseRequest(url,immediate,method,headers,timeout){}⬆ 返回目录四、输入输出设计边界要清晰4.1 输入尽量用「配置对象」而不是「一长串参数」// ❌ 不推荐参数一多就很难记顺序functionuseTable(config,pageSize,currentPage,sortField,sortOrder){}// ✅ 推荐用对象 默认值functionuseTable(options{}){const{pageSize10,currentPage1,sortFieldid,sortOrderasc}options// ...}这样既能扩展参数又不会破坏现有调用。⬆ 返回目录4.2 输出只暴露必要内容// ❌ 不好把内部细节也暴露了functionuseUserList(){constloadingref(false)consterrorref(null)constinternalCacheref({})// 内部缓存不该暴露const_fetchData(){}// 内部方法不该暴露return{loading,error,internalCache,_fetchData}}// ✅ 好只暴露「用户需要」的functionuseUserList(){constloadingref(false)consterrorref(null)constusersref([])constrefetchasync(){/* ... */}return{loading,error,users,refetch}原则只返回外部会用到的状态和方法内部实现细节一律不暴露。⬆ 返回目录4.3 完整示例useRequest 的输入输出设计// useRequest.jsimport{ref,watch}fromvue/** * 通用请求 Hook * param {string} url - 请求地址 * param {Object} options - 配置项 * param {boolean} options.immediate - 是否立即请求默认 true * param {Object} options.params - 请求参数变化时自动重新请求 */exportfunctionuseRequest(url,options{}){const{immediatetrue,params{}}optionsconstdataref(null)constloadingref(false)consterrorref(null)constexecuteasync(){loading.valuetrueerror.valuenulltry{constresawaitfetch(${url}?${newURLSearchParams(params)})data.valueawaitres.json()returndata.value}catch(e){error.valueethrowe}finally{loading.valuefalse}}// params 变化时重新请求watch(()({...params}),execute,{immediate})return{data,loading,error,execute}// 组件中使用import{useRequest}from/hooks/useRequestexportdefault{setup(){const{data,loading,error,execute}useRequest(/api/users,{params:{page:1,size:10},immediate:true})return{data,loading,error,execute}}}要点输入urloptions语义清晰输出data、loading、error、execute都是外部会用到的内部逻辑完全封装在函数内⬆ 返回目录五、复用边界什么时候该抽、抽成什么5.1 判断标准3 次原则只用了 1 次直接在组件里写用了 2 次可以先复制粘贴确认会长期复用再抽用了 3 次及以上建议抽成组合式函数不要为了“显得高级”而过早抽象。⬆ 返回目录5.2 常见边界划分场景适合放哪理由纯 UI 状态如弹窗开关组件内和当前组件强相关跨组件复用的业务逻辑组合式函数逻辑可复用视图各自定义纯工具函数无响应式普通工具函数不是 Hook不必use开头全局状态Pinia/Vuex跨很多组件共享⬆ 返回目录5.3 反例把业务写死在 Hook 里// ❌ 错误Hook 里写死了「用户列表」业务functionuseUserList(){constusersref([])constfetchUsersasync(){constresawaitfetch(/api/users)// 写死接口users.valueawaitres.json()}return{users,fetchUsers}问题换接口、换业务就要改 Hook复用性差。// ✅ 正确Hook 只负责「请求流程」业务由调用方决定functionuseRequest(url,options{}){constdataref(null)constloadingref(false)constexecuteasync(){loading.valuetruetry{constresawaitfetch(url)data.valueawaitres.json()}finally{loading.valuefalse}}return{data,loading,execute}}// 使用时传入不同 url 即可const{data:users,loading,execute}useRequest(/api/users)const{data:products,loading:loadingProducts}useRequest(/api/products)⬆ 返回目录5.4 完整示例useCountdown 的边界// useCountdown.jsimport{ref,onUnmounted}fromvue/** * 倒计时 Hook * 职责倒计时逻辑开始、暂停、重置 * 不职责显示样式、文案 */exportfunctionuseCountdown(initialSeconds60){constremainingref(initialSeconds)constisRunningref(false)lettimernullconststart(){if(isRunning.value)returnisRunning.valuetruetimersetInterval((){remaining.value--if(remaining.value0){stop()}},1000)}conststop(){clearInterval(timer)timernullisRunning.valuefalse}constreset(){stop()remaining.valueinitialSeconds}onUnmounted(stop)return{remaining,isRunning,start,stop,reset}!-- 验证码倒计时组件 --templatebutton:disabledisRunningclickhandleSend{{ isRunning ? ${remaining}秒后重试 : 发送验证码 }}/button/templatescriptsetupimport{useCountdown}from/hooks/useCountdownconst{remaining,isRunning,start,reset}useCountdown(60)consthandleSendasync(){awaitsendSmsApi()// 业务接口reset()start()}/script要点Hook 只负责“倒计时状态与行为”文案、样式、业务接口都在组件里⬆ 返回目录六、与 Vue 组件、模板的配合规范6.1 组件只负责「组合」和「视图」!-- UserList.vue --templatedivclassuser-listdivv-ifloading加载中.../divdivv-else-iferror加载失败/divulv-elseliv-foruser in users:keyuser.id{{ user.name }}/li/ulbuttonclickrefetch刷新/button/div/templatescriptsetup// 组件 组合多个 Hook 定义模板import{useRequest}from/hooks/useRequestconst{data:users,loading,error,execute:refetch}useRequest(/api/users)/script组件 组合式函数 模板职责清晰。⬆ 返回目录6.2 模板里避免复杂逻辑!-- ❌ 不好模板里一堆计算 --templatespan{{ items.filter(i i.checked).length }} / {{ items.length }}/span/template!-- ✅ 好用 computed 抽出来 --templatespan{{ checkedCount }} / {{ totalCount }}/span/templatescriptsetupconstcheckedCountcomputed(()items.value.filter(ii.checked).length)consttotalCountcomputed(()items.value.length)/script⬆ 返回目录6.3 组件 props 与 Hooks 的配合// 当 Hook 需要依赖组件 props 时用 watch/watchEffect 或传参functionuseUserDetail(userId){constuserref(null)watch(()userId,async(id){if(!id)returnuser.valueawaitfetchUser(id)},{immediate:true})return{user}}// 组件中constpropsdefineProps([userId])const{user}useUserDetail(()props.userId)// 传 getter 也行⬆ 返回目录七、常见坑与避坑指南7.1 忘记处理响应式// ❌ 错误直接解构会丢失响应式functionuseCounter(){return{count:ref(0),increment:(){}}}const{count}useCounter()// count 在模板中不会更新// ✅ 正确返回 ref使用时保持 refconst{count,increment}useCounter()// 模板中直接用 count或用 count.value 访问在script setup顶层解构 ref 是没问题的模板会自动解包在普通函数里再解构就要注意保持 ref 引用。⬆ 返回目录7.2 生命周期和副作用没清理// ❌ 错误定时器、事件监听没有清理functionusePolling(callback,interval){setInterval(callback,interval)// 组件销毁后仍在执行return{}}// ✅ 正确在 onUnmounted 中清理functionusePolling(callback,interval){consttimersetInterval(callback,interval)onUnmounted(()clearInterval(timer))return{}}⬆ 返回目录7.3 把组合式函数当工具函数用// ❌ 错误在非 setup 中调用没有响应式上下文asyncfunctionhandleClick(){const{data}useRequest(/api/xxx)// 每次点击都新建不会累积await...}组合式函数必须在setup或script setup的顶层同步调用不能放在事件回调、异步函数里。⬆ 返回目录八、小结一套可直接照搬的检查清单封装组合式函数时可以按下面 checklist 自检命名以use开头能看出功能和用途输入参数尽量用对象 默认值不把业务细节写死在 Hook 里输出只返回外部会用到的内容不暴露内部实现细节复用边界满足 3 次再用再抽区分纯逻辑 vs 业务 vs 全局状态副作用定时器、监听等在onUnmounted中清理照着这些规范来组合式函数的可读性和复用性都会好很多。后面可以根据自己项目再逐步补充团队约定。⬆ 返回目录 系列模块导航 编码语法规范这是前端规范实战系列中第二个模块当编码语法规范模块更新完成之后会附上此模块的跳转链接方便同学们阅读学习。更新中敬请期待~ 跟着系列慢慢学把技术功底扎扎实实地打牢 系列总览「前端规范实战系列」正在持续更新中后续会整理一篇《前端规范实战系列全系列目录导航》包含每篇文章简介 直达链接方便大家按顺序、体系化学习。更新中敬请期待⬆ 返回目录技术成长从来不是比谁写得快而是比谁写得稳、规范、可维护。哪怕每次只吃透一条规范长期下来差距会非常明显。后续我会持续更新前端规范、工程化、可维护代码相关实战干货帮你告别面条代码、维护噩梦在开发与面试中更有底气。觉得有用欢迎点赞 收藏 关注不错过每一篇实战内容。我是 Eugene与你一起写规范、写优质代码我们下篇干货见