Vue3 组合式函数(Hooks)封装规范实战:命名 / 输入输出 / 复用边界 + 避坑|Vue 组件与模板规范篇

【Vue3 组合式函数】+【前端开发实战】:从命名、输入输出到复用边界,彻底搞懂组合式函数的最佳封装写法,避开响应式丢失、副作用未清理等高频坑!

📑 文章目录

  • 一、前言:为什么要单独讲「组合式函数」?
  • 二、组合式函数到底是什么?
  • 三、命名规范:一看就知道用途
    • [3.1 函数命名:use + 动词/名词](#3.1 函数命名:use + 动词/名词)
    • [3.2 返回值命名:结构清晰、语义明确](#3.2 返回值命名:结构清晰、语义明确)
    • [3.3 参数命名:一看就懂用法](#3.3 参数命名:一看就懂用法)
  • 四、输入输出设计:边界要清晰
    • [4.1 输入:尽量用「配置对象」而不是「一长串参数」](#4.1 输入:尽量用「配置对象」而不是「一长串参数」)
    • [4.2 输出:只暴露必要内容](#4.2 输出:只暴露必要内容)
    • [4.3 完整示例:useRequest 的输入输出设计](#4.3 完整示例:useRequest 的输入输出设计)
  • 五、复用边界:什么时候该抽、抽成什么
    • [5.1 判断标准:3 次原则](#5.1 判断标准:3 次原则)
    • [5.2 常见边界划分](#5.2 常见边界划分)
    • [5.3 反例:把业务写死在 Hook 里](#5.3 反例:把业务写死在 Hook 里)
    • [5.4 完整示例:useCountdown 的边界](#5.4 完整示例:useCountdown 的边界)
  • [六、与 Vue 组件、模板的配合规范](#六、与 Vue 组件、模板的配合规范)
    • [6.1 组件只负责「组合」和「视图」](#6.1 组件只负责「组合」和「视图」)
    • [6.2 模板里避免复杂逻辑](#6.2 模板里避免复杂逻辑)
    • [6.3 组件 props 与 Hooks 的配合](#6.3 组件 props 与 Hooks 的配合)
  • 七、常见坑与避坑指南
    • [7.1 忘记处理响应式](#7.1 忘记处理响应式)
    • [7.2 生命周期和副作用没清理](#7.2 生命周期和副作用没清理)
    • [7.3 把组合式函数当工具函数用](#7.3 把组合式函数当工具函数用)
  • 八、小结:一套可直接照搬的检查清单
  • [🔍 系列模块导航](#🔍 系列模块导航)

同学们好,我是 Eugene(尤金),一名多年中后台前端开发工程师。

(Eugene 发音 /juːˈdʒiːn/,大家怎么顺口怎么叫就好)

很多前端开发者都会遇到一个瓶颈:

代码能跑,但不够规范;功能能实现,但维护起来特别痛苦;一个人写没问题,一到团队协作就各种混乱、踩坑、返工。

想写出干净、优雅、可维护 的专业代码,靠的不是天赋,而是体系化的规范 + 真实实战经验

这一系列《前端规范实战》,我会用大白话 + 真实业务场景,不讲玄学、不堆理论,只分享能直接落地的规范、标准与避坑指南。

帮你从「会写代码」真正升级为「会写优质、可维护、团队级别的代码」。


一、前言:为什么要单独讲「组合式函数」?

如果你已经能写 Vue 组件,但总觉得:

  • 抽逻辑时不知道往哪儿放
  • 自己写的 useXxx 别人难复用
  • 复用逻辑时容易和业务强耦合

多半是组合式函数(Composition API / Hooks)的边界没理清。

这篇文章不讲底层原理,只讲:日常写代码该怎么选、为什么这么选、坑会出在哪儿。目标是帮你形成一套可落地的封装规范,并在业务里持续复用。

[⬆ 返回目录](#⬆ 返回目录)


二、组合式函数到底是什么?

简单说:把可复用的响应式逻辑抽到一个函数里,这个函数就叫组合式函数。

它必须:

  • use 开头命名(约定俗成)
  • 只处理逻辑,不渲染模板
  • 可以被多个组件重复调用

组合式函数 ≠ 组件,它是"逻辑单元",不是"视图单元"。

[⬆ 返回目录](#⬆ 返回目录)


三、命名规范:一看就知道用途

3.1 函数命名:use + 动词/名词

javascript 复制代码
// ✅ 好的命名:动词表示行为
useToggle()        // 切换状态
useRequest()       // 发请求
useLocalStorage()  // 读写本地存储
useDebounce()      // 防抖

// ✅ 好的命名:名词表示能力
useCountdown()     // 倒计时
usePagination()    // 分页
useTableSelection()// 表格选中

// ❌ 不好的命名:含义模糊
useData()          // 什么数据?干什么用?
useHandle()        // 处理什么?
useLogic()         // 逻辑太泛

原则:从名字就能猜出功能和职责

[⬆ 返回目录](#⬆ 返回目录)

3.2 返回值命名:结构清晰、语义明确

javascript 复制代码
// ✅ 推荐:对象解构,名字和用途一一对应
function useCounter(initial = 0) {
  const count = ref(initial)
  const increment = () => count.value++
  const decrement = () => count.value--
  return { count, increment, decrement }
}
// 使用时:const { count, increment, decrement } = useCounter()

// ✅ 也可以:数组返回,适合顺序固定、数量少的场景
function useToggle(initial = false) {
  const state = ref(initial)
  const toggle = () => { state.value = !state.value }
  return [state, toggle]
}
// 使用时:const [isVisible, toggleVisible] = useToggle()

尽量避免:返回值里既有数据又有方法,名字却很抽象(如 datamethods),别人很难猜到怎么用。

[⬆ 返回目录](#⬆ 返回目录)

3.3 参数命名:一看就懂用法

javascript 复制代码
// ✅ 好:参数有默认值、含义清晰
function useRequest(url, options = {}) {
  const { immediate = true, method = 'GET' } = options
  // ...
}

// ✅ 好:用对象收拢可选参数
function usePagination({
  pageSize = 10,
  total = 0,
  currentPage = 1
} = {}) {
  // ...
}

// ❌ 差:参数多且无默认值,调用方容易传错
function useRequest(url, immediate, method, headers, timeout) { }

[⬆ 返回目录](#⬆ 返回目录)


四、输入输出设计:边界要清晰

4.1 输入:尽量用「配置对象」而不是「一长串参数」

javascript 复制代码
// ❌ 不推荐:参数一多就很难记顺序
function useTable(config, pageSize, currentPage, sortField, sortOrder) { }

// ✅ 推荐:用对象 + 默认值
function useTable(options = {}) {
  const {
    pageSize = 10,
    currentPage = 1,
    sortField = 'id',
    sortOrder = 'asc'
  } = options
  // ...
}

这样既能扩展参数,又不会破坏现有调用。

[⬆ 返回目录](#⬆ 返回目录)

4.2 输出:只暴露必要内容

javascript 复制代码
// ❌ 不好:把内部细节也暴露了
function useUserList() {
  const loading = ref(false)
  const error = ref(null)
  const internalCache = ref({})  // 内部缓存,不该暴露
  const _fetchData = () => {}    // 内部方法,不该暴露
  return { loading, error, internalCache, _fetchData }
}

// ✅ 好:只暴露「用户需要」的
function useUserList() {
  const loading = ref(false)
  const error = ref(null)
  const users = ref([])
  const refetch = async () => { /* ... */ }
  return { loading, error, users, refetch }

原则:只返回外部会用到的状态和方法,内部实现细节一律不暴露

[⬆ 返回目录](#⬆ 返回目录)

4.3 完整示例:useRequest 的输入输出设计

javascript 复制代码
// useRequest.js
import { ref, watch } from 'vue'

/**
 * 通用请求 Hook
 * @param {string} url - 请求地址
 * @param {Object} options - 配置项
 * @param {boolean} options.immediate - 是否立即请求,默认 true
 * @param {Object} options.params - 请求参数,变化时自动重新请求
 */
export function useRequest(url, options = {}) {
  const { immediate = true, params = {} } = options
  
  const data = ref(null)
  const loading = ref(false)
  const error = ref(null)
  
  const execute = async () => {
    loading.value = true
    error.value = null
    try {
      const res = await fetch(`${url}?${new URLSearchParams(params)}`)
      data.value = await res.json()
      return data.value
    } catch (e) {
      error.value = e
      throw e
    } finally {
      loading.value = false
    }
  }
  
  // params 变化时重新请求
  watch(() => ({ ...params }), execute, { immediate })
  
  return { data, loading, error, execute }
javascript 复制代码
// 组件中使用
import { useRequest } from '@/hooks/useRequest'

export default {
  setup() {
    const { data, loading, error, execute } = useRequest('/api/users', {
      params: { page: 1, size: 10 },
      immediate: true
    })
    return { data, loading, error, execute }
  }
}

要点:

  • 输入:url + options,语义清晰
  • 输出:dataloadingerrorexecute,都是外部会用到的
  • 内部逻辑完全封装在函数内

[⬆ 返回目录](#⬆ 返回目录)


五、复用边界:什么时候该抽、抽成什么

5.1 判断标准:3 次原则

  • 只用了 1 次:直接在组件里写
  • 用了 2 次:可以先复制粘贴,确认会长期复用再抽
  • 用了 3 次及以上:建议抽成组合式函数

不要为了"显得高级"而过早抽象。

[⬆ 返回目录](#⬆ 返回目录)

5.2 常见边界划分

场景 适合放哪 理由
纯 UI 状态(如弹窗开关) 组件内 和当前组件强相关
跨组件复用的业务逻辑 组合式函数 逻辑可复用,视图各自定义
纯工具函数(无响应式) 普通工具函数 不是 Hook,不必 use 开头
全局状态 Pinia/Vuex 跨很多组件共享

[⬆ 返回目录](#⬆ 返回目录)

5.3 反例:把业务写死在 Hook 里

javascript 复制代码
// ❌ 错误:Hook 里写死了「用户列表」业务
function useUserList() {
  const users = ref([])
  const fetchUsers = async () => {
    const res = await fetch('/api/users')  // 写死接口
    users.value = await res.json()
  }
  return { users, fetchUsers }

问题:换接口、换业务就要改 Hook,复用性差。

javascript 复制代码
// ✅ 正确:Hook 只负责「请求流程」,业务由调用方决定
function useRequest(url, options = {}) {
  const data = ref(null)
  const loading = ref(false)
  const execute = async () => {
    loading.value = true
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } finally {
      loading.value = false
    }
  }
  return { data, loading, execute }
}
// 使用时传入不同 url 即可
const { data: users, loading, execute } = useRequest('/api/users')
const { data: products, loading: loadingProducts } = useRequest('/api/products')

[⬆ 返回目录](#⬆ 返回目录)

5.4 完整示例:useCountdown 的边界

javascript 复制代码
// useCountdown.js
import { ref, onUnmounted } from 'vue'

/**
 * 倒计时 Hook
 * 职责:倒计时逻辑(开始、暂停、重置)
 * 不职责:显示样式、文案
 */
export function useCountdown(initialSeconds = 60) {
  const remaining = ref(initialSeconds)
  const isRunning = ref(false)
  let timer = null
  
  const start = () => {
    if (isRunning.value) return
    isRunning.value = true
    timer = setInterval(() => {
      remaining.value--
      if (remaining.value <= 0) {
        stop()
      }
    }, 1000)
  }
  
  const stop = () => {
    clearInterval(timer)
    timer = null
    isRunning.value = false
  }
  
  const reset = () => {
    stop()
    remaining.value = initialSeconds
  }
  
  onUnmounted(stop)
  
  return { remaining, isRunning, start, stop, reset }
html 复制代码
<!-- 验证码倒计时组件 -->
<template>
  <button 
    :disabled="isRunning" 
    @click="handleSend"
  >
    {{ isRunning ? `${remaining}秒后重试` : '发送验证码' }}
  </button>
</template>

<script setup>
import { useCountdown } from '@/hooks/useCountdown'

const { remaining, isRunning, start, reset } = useCountdown(60)

const handleSend = async () => {
  await sendSmsApi()  // 业务接口
  reset()
  start()
}
</script>

要点:

  • Hook 只负责"倒计时状态与行为"
  • 文案、样式、业务接口都在组件里

[⬆ 返回目录](#⬆ 返回目录)


六、与 Vue 组件、模板的配合规范

6.1 组件只负责「组合」和「视图」

html 复制代码
<!-- UserList.vue -->
<template>
  <div class="user-list">
    <div v-if="loading">加载中...</div>
    <div v-else-if="error">加载失败</div>
    <ul v-else>
      <li v-for="user in users" :key="user.id">{{ user.name }}</li>
    </ul>
    <button @click="refetch">刷新</button>
  </div>
</template>

<script setup>
// 组件 = 组合多个 Hook + 定义模板
import { useRequest } from '@/hooks/useRequest'

const { data: users, loading, error, execute: refetch } = useRequest('/api/users')
</script>

组件 = 组合式函数 + 模板,职责清晰。

[⬆ 返回目录](#⬆ 返回目录)

6.2 模板里避免复杂逻辑

html 复制代码
<!-- ❌ 不好:模板里一堆计算 -->
<template>
  <span>{{ items.filter(i => i.checked).length }} / {{ items.length }}</span>
</template>

<!-- ✅ 好:用 computed 抽出来 -->
<template>
  <span>{{ checkedCount }} / {{ totalCount }}</span>
</template>

<script setup>
const checkedCount = computed(() => items.value.filter(i => i.checked).length)
const totalCount = computed(() => items.value.length)
</script>

[⬆ 返回目录](#⬆ 返回目录)

6.3 组件 props 与 Hooks 的配合

javascript 复制代码
// 当 Hook 需要依赖组件 props 时,用 watch/watchEffect 或传参
function useUserDetail(userId) {
  const user = ref(null)
  
  watch(() => userId, async (id) => {
    if (!id) return
    user.value = await fetchUser(id)
  }, { immediate: true })
  
  return { user }
}

// 组件中
const props = defineProps(['userId'])
const { user } = useUserDetail(() => props.userId)  // 传 getter 也行

[⬆ 返回目录](#⬆ 返回目录)


七、常见坑与避坑指南

7.1 忘记处理响应式

javascript 复制代码
// ❌ 错误:直接解构会丢失响应式
function useCounter() {
  return { count: ref(0), increment: () => {} }
}
const { count } = useCounter()
// count 在模板中不会更新!
javascript 复制代码
// ✅ 正确:返回 ref,使用时保持 ref
const { count, increment } = useCounter()
// 模板中直接用 count,或用 count.value 访问

<script setup> 顶层解构 ref 是没问题的,模板会自动解包;在普通函数里再解构就要注意保持 ref 引用。

[⬆ 返回目录](#⬆ 返回目录)

7.2 生命周期和副作用没清理

javascript 复制代码
// ❌ 错误:定时器、事件监听没有清理
function usePolling(callback, interval) {
  setInterval(callback, interval)  // 组件销毁后仍在执行
  return {}
}
javascript 复制代码
// ✅ 正确:在 onUnmounted 中清理
function usePolling(callback, interval) {
  const timer = setInterval(callback, interval)
  onUnmounted(() => clearInterval(timer))
  return {}
}

[⬆ 返回目录](#⬆ 返回目录)

7.3 把组合式函数当工具函数用

javascript 复制代码
// ❌ 错误:在非 setup 中调用,没有响应式上下文
async function handleClick() {
  const { data } = useRequest('/api/xxx')  // 每次点击都新建,不会累积
  await ...
}

组合式函数必须在 setup<script setup> 的顶层同步调用,不能放在事件回调、异步函数里。

[⬆ 返回目录](#⬆ 返回目录)


八、小结:一套可直接照搬的检查清单

封装组合式函数时,可以按下面 checklist 自检:

  1. 命名
  • use 开头
  • 能看出功能和用途
  1. 输入
  • 参数尽量用对象 + 默认值
  • 不把业务细节写死在 Hook 里
  1. 输出
  • 只返回外部会用到的内容
  • 不暴露内部实现细节
  1. 复用边界
  • 满足 3 次再用再抽
  • 区分:纯逻辑 vs 业务 vs 全局状态
  1. 副作用
  • 定时器、监听等在 onUnmounted 中清理

照着这些规范来,组合式函数的可读性和复用性都会好很多。

[⬆ 返回目录](#⬆ 返回目录)

🔍 系列模块导航

📝 Vue 组件与模板规范

一、《Vue3 组件拆分实战规范:页面 / 业务 / 基础组件边界清晰化,高内聚低耦合落地指南|Vue 组件与模板规范篇》
二、《Vue3 Props 传参实战规范:必传校验 + 默认值 + 类型标注,避开 undefined / 类型混用坑|Vue 组件与模板规范篇》
三、《Vue3 模板语法规范实战:v-if/v-for 不混用 + 表达式精简,避坑指南|Vue 组件与模板规范篇》
四、《Vue3 样式实战:scoped + 深度选择器 + BEM 规范,解决冲突与穿透失效|Vue 组件与模板规范篇》

五、《Vue3 组合式函数(Hooks)封装规范实战:命名 / 输入输出 / 复用边界 + 避坑|Vue 组件与模板规范篇》
六、《Vue3 + Element Plus 中后台弹窗规范:开闭、传参、回调,告别弹窗地狱|Vue 组件与模板规范篇》
七、《Vue3 组件解耦实战:Props/Emit/ 事件总线用法 + 避坑指南|Vue 组件与模板规范篇》

👉 跟着系列慢慢学,把技术功底扎扎实实地打牢~

📚 系列总览

前端规范实战系列 」正在持续更新中,后续会整理一篇《前端规范实战系列全系列目录导航》,包含每篇文章简介 + 直达链接,方便大家按顺序、体系化学习。

更新中,敬请期待~

[⬆ 返回目录](#⬆ 返回目录)


技术成长,从来不是比谁写得快,而是比谁写得稳、规范、可维护

哪怕每次只吃透一条规范,长期下来,差距会非常明显。

后续我会持续更新前端规范、工程化、可维护代码相关实战干货,帮你告别面条代码、维护噩梦,在开发与面试中更有底气。

觉得有用欢迎 点赞 + 收藏 + 关注,不错过每一篇实战内容。

我是 Eugene,与你一起写规范、写优质代码,我们下篇干货见~

相关推荐
喝拿铁写前端31 分钟前
一套面向 Web、H5、小程序与 Flutter 的多端一致性技术方案
前端·架构
小碗羊肉35 分钟前
【从零开始学Java | 第十八篇】BigInteger
java·开发语言·新手入门
yaaakaaang36 分钟前
(一)前端,如此简单!---下载Nginx
前端·nginx
宵时待雨37 分钟前
C++笔记归纳14:AVL树
开发语言·数据结构·c++·笔记·算法
牛奶42 分钟前
为什么全国人民都能秒开同一个视频?
前端·http·cdn
执笔画流年呀1 小时前
PriorityQueue(堆)续集
java·开发语言
山川行1 小时前
关于《项目C语言》专栏的总结
c语言·开发语言·数据结构·vscode·python·算法·visual studio code
呜喵王阿尔萨斯1 小时前
C and C++ code
c语言·开发语言·c++
左左右右左右摇晃1 小时前
JDK 1.7 ConcurrentHashMap——分段锁
java·开发语言·笔记
xcLeigh1 小时前
Python入门:Python3基础练习题详解,从入门到熟练的 25 个实例(六)
开发语言·python·教程·python3·练习题