人人都会AI编程

11.1 自定义 Hooks 的设计原则与命名规范

更新时间:2026-07-11

Vue 3 的组合式 API 除了提供 refcomputedwatch 等基础工具外,最重要的能力就是将逻辑封装成可复用的函数,这类函数通常被称为“组合式函数”(Composables),也就是社区常说的“自定义 Hooks”。

一个设计良好的自定义 Hook 能让多个组件共享同一段业务逻辑,避免重复代码,同时保持组件的清晰和可测试。但随意封装也会带来“过度抽象”和“难以维护”的问题,所以需要遵循一些务实的原则和命名约定。


设计原则

1. 单一职责:一个 Hook 只做一件事

每个自定义 Hook 应该围绕一个明确的功能点展开,比如处理表单校验、管理分页状态、封装鼠标位置追踪。不要试图把多个不相关的逻辑塞进同一个 Hook,否则它会变成一个“杂物间”,时间一长谁都看不懂。

// ✅ 好的设计:职责单一
export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  // ... 跟踪鼠标位置
  return { x, y };
}

// ❌ 坏的设计:把不相关的逻辑混在一起
export function useDashboard() {
  const mouse = useMouse();
  const users = useFetch('/api/users');
  const form = useForm();
  // ... 杂糅了鼠标、请求、表单,抽象层级混乱
  return { mouse, users, form };
}

单一职责的 Hook 更容易组合:你可以在组件里按需引入多个局部的 Hook,而不是依赖一个巨大且耦合的“全家桶”。

2. 输入与输出清晰:像纯函数一样思考

虽然 Hook 内部通常包含副作用(如事件监听、数据请求),但它的对外接口应该尽可能“干净”:

  • 参数:明确需要的配置项,尽量使用对象形式(options),避免参数位置记忆负担。
  • 返回值:只暴露组件真正需要的数据和方法,不要把不需要的内部状态泄露出去。如果返回的值很多,考虑拆分成更小的 Hook。
// 使用 options 对象,调用意图更清晰
function usePagination({ pageSize = 10, total = 0 } = {}) {
  const currentPage = ref(1);
  const totalPages = computed(() => Math.ceil(total / pageSize));
  // ...
  return { currentPage, totalPages, nextPage, prevPage };
}

避免在 Hook 外部直接修改内部的 ref,而是提供明确的方法来改变状态,这样可以保持状态变更的可控性。

3. 避免过早抽象,保持真实

不是所有 refcomputed 的组合都必须封装成 Hook。有一个很实用的判断标准:当同一个逻辑至少在两个不同的组件中被复制粘贴时,再考虑抽离;如果只在一个地方用,那只管写在组件里。因为过早抽象会增加理解和修改的成本,你无法预测未来的复用模式。

4. 副作用要可清理

自定义 Hook 中如果注册了全局事件监听、定时器或者创建了某些外部资源,必须在合适的时机进行清理。Vue 的组合式 API 提供了 onBeforeUnmountonDeactivated(配合 keep-alive)等生命周期钩子来执行清理工作。

export function useEventListener(target, event, callback) {
  onMounted(() => target.addEventListener(event, callback));
  onBeforeUnmount(() => target.removeEventListener(event, callback));
}

忘记清理是“内存泄漏”的头号元凶,必须养成习惯。

5. 响应式依赖要保持连接

自定义 Hook 返回的响应式数据(refreactivecomputed)应该保持“活”的引用,而不是在返回前解构为普通值。这样组件才能继续追踪依赖,保持响应式更新。

// ✅ 正确:返回 ref 本身
export function useCounter() {
  const count = ref(0);
  const increment = () => { count.value++; };
  return { count, increment };
}

// ❌ 错误:返回 .value,变成了普通值,失去了响应式
export function useBrokenCounter() {
  const count = ref(0);
  return count.value; // 只是一个静态的 0
}

命名规范

1. 必须使用 use 前缀

Vue 社区约定,所有组合式函数的文件名和函数名都以 use 开头。这样做的好处是:

  • 一眼可分辨useMouseuseFetch 显然是组合式函数,而 formatDatevalidateEmail 则是普通工具函数。
  • 工具支持:ESLint 插件和 IDE 能够识别这类函数,并正确应用组合式 API 的规则(比如必须放在 setup 顶层、不能在条件内调用等)。

| 类型 | 命名示例 | 用途 |
|------|----------|------|
| 状态管理 | useUserStoreuseCart | 全局或局部状态 |
| 数据请求 | useFetchUsersuseDebouncedRequest | 接口封装 |
| 浏览器能力 | useWindowSizeuseLocalStorageuseEventListener | 操作外部 API |
| 业务逻辑 | useFormValidationusePaginationuseAuth | 具体业务抽象 |

2. 文件命名与函数名保持一致

文件名也应遵循 useXxx.js(或 .ts)的格式。例如:

src/composables/
  useMouse.js
  useFetch.js
  useAuth.js

如果项目中有大量 hooks,可以按功能分目录:

src/composables/
  core/          # 底层通用 hooks(浏览器 API、工具函数)
    useEventListener.js
    useLocalStorage.js
  business/      # 业务相关 hooks
    useOrderList.js
    usePermission.js

3. 组件中使用时保持命名可读性

当在组件中调用 Hook 时,可以用 JavaScript 的解构语法提取需要的部分,但注意避免命名冲突。通常建议调用时保持函数名原样,或者根据上下文稍作别名:

// 直接使用
const { x, y } = useMouse();

// 有冲突时起别名
const { x: scrollX, y: scrollY } = useScroll();
const { x: mouseX, y: mouseY } = useMouse();

真实例子:一个分页 Hook 的完整形态

将以上原则用一个实际场景串联起来:封装一个可复用的分页逻辑。

// composables/usePagination.js
import { ref, computed } from 'vue';

export function usePagination({ total = ref(0), pageSize = 10 } = {}) {
  const currentPage = ref(1);

  const totalPages = computed(() => Math.ceil(total.value / pageSize));

  const hasNext = computed(() => currentPage.value < totalPages.value);
  const hasPrev = computed(() => currentPage.value > 1);

  function nextPage() {
    if (hasNext.value) currentPage.value++;
  }

  function prevPage() {
    if (hasPrev.value) currentPage.value--;
  }

  function goToPage(page) {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page;
    }
  }

  return {
    currentPage,   // 响应式当前页
    totalPages,    // 总页数
    hasNext,
    hasPrev,
    nextPage,
    prevPage,
    goToPage,
  };
}

组件中使用:

<script setup>
import { ref } from 'vue';
import { usePagination } from '@/composables/usePagination';

const totalItems = ref(100);
const { currentPage, totalPages, hasNext, hasPrev, nextPage, prevPage } =
  usePagination({ total: totalItems, pageSize: 10 });
</script>

这样的 Hook 单一职责明确(只管分页计算),输入通过 options 配置,输出一组响应式状态和方法,命名以 use 开头,文件放在 composables/ 下,完全符合规范。


遵循这些原则和命名规范,自定义 Hook 就会成为你项目中逻辑复用的标准模块,而不会变成令人头疼的“黑盒包袱”。当你发现多个组件开始出现重复的 refcomputed 组合时,就可以问自己:“这块逻辑是否可以提炼成一个 useXxx?”