Composable的命名规矩和参数约定,别再瞎写了

0 阅读5分钟

一、为啥要有约定?你乱写别人看不懂啊

你有没有接过别人的项目,打开代码一看——getDatafetchInfomouseHelperwindowSizeTool……啥名字都有,完全不知道哪个是Composable,哪个是普通工具函数。

约定存在的意义就是让代码有"可预测性"。你看到use开头就知道这是个Composable,看到返回值就知道该怎么解构,不用猜、不用问、不用翻文档。

Vue社区对Composables有一套公认的约定,虽然不是强制的,但大家都这么干,你跟着走准没错。

二、命名约定:use开头,驼峰走起

函数名以use开头

这是最最最重要的约定,没有之一。所有Composable函数名都以use开头:

// ✅ 正确的命名
export function useMouse() { ... }
export function useFetch() { ... }
export function useLocalStorage() { ... }
export function useDebounce() { ... }

// ❌ 别这么写
export function getMousePosition() { ... }
export function fetchHelper() { ... }
export function localStorageManager() { ... }

为啥一定要use开头?因为:

  1. 一眼识别:看到use就知道这是个Composable,不是普通函数
  2. 语义清晰useMouse = "使用鼠标追踪功能",useFetch = "使用数据请求功能"
  3. 社区统一:VueUse(最流行的Composable库)里几百个函数全是use开头

用驼峰命名法

// ✅ 驼峰命名
export function useWindowSize() { ... }
export function useScrollPosition() { ... }

// ❌ 别用下划线
export function use_window_size() { ... }
export function use_scroll_position() { ... }

// ❌ 别用短横线(JS函数名不允许)
export function use-window-size() { ... }

文件名和函数名保持一致

// 文件名:useMouse.js
export function useMouse() { ... }

// 文件名:useFetch.js
export function useFetch() { ... }

这样别人看到文件名就知道里面导出的是啥函数,不用打开文件去看。

flowchart LR
    A[文件名] -->|保持一致| B[函数名]
    B -->|use前缀| C[一眼识别为Composable]
    B -->|驼峰命名| D[符合JS命名规范]
    A --> E[useMouse.js → useMouse]
    A --> F[useFetch.js → useFetch]
    A --> G[useLocalStorage.js → useLocalStorage]

三、输入参数约定:能接收ref和getter才够灵活

问题来了

假设你写了个useFetch,接收一个URL字符串:

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);

  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err));

  return { data, error };
}

用起来是这样的:

const { data } = useFetch("/api/users"); // 传个字符串,没问题

但问题来了——如果URL是动态的呢?比如根据路由参数变化来请求不同的数据:

// 这个不会自动重新请求!
const url = ref("/api/users/1");
const { data } = useFetch(url.value); // 传的是字符串值,不是ref

url.value = "/api/users/2"; // URL变了,但不会重新fetch

解决方案:用toValue兼容多种输入

Vue 3.3新增了toValue()这个API,专门用来处理这种"输入可能是字符串、也可能是ref、还可能是getter函数"的情况:

import { toValue } from "vue";

export function useFetch(url) {
  const data = ref(null);
  const error = ref(null);

  // toValue() 的行为:
  // - 如果是 ref → 返回 ref.value
  // - 如果是函数 → 调用函数并返回结果
  // - 如果是普通值 → 原样返回
  const resolvedUrl = toValue(url);

  fetch(resolvedUrl)
    .then((res) => res.json())
    .then((json) => (data.value = json))
    .catch((err) => (error.value = err));

  return { data, error };
}

这样不管调用方传字符串、ref还是getter,都能正常工作:

// 传字符串
useFetch("/api/users");

// 传ref
const url = ref("/api/users");
useFetch(url);

// 传getter函数
useFetch(() => `/api/users/${props.id}`);

toValue和unref有啥区别?

你可能会问,unref()不也能把ref变成值吗?它俩的区别在于:

输入unref()toValue()
ref('hello')'hello''hello'
'hello''hello''hello'
() => 'hello'原样返回函数'hello'(调用函数)

toValue()多了一步——如果输入是函数,它会调用这个函数并返回结果。所以当你需要兼容getter函数时,用toValue()更合适。

什么时候该用toValue?

不是所有Composable都需要用toValue。只有当你的参数可能需要是响应式的时候才用:

// ✅ 需要用toValue的场景
// URL可能动态变化
export function useFetch(url) {
  const value = toValue(url);
}

// ✅ 需要用toValue的场景
// 目标元素可能动态变化
export function useEventListener(target, event, callback) {
  const el = toValue(target);
}

// ❌ 不需要toValue的场景
// 配置项一般是静态的
export function useTheme(options) {
  // options就是个普通对象,不需要toValue
}

四、返回值约定:ref包在普通对象里

核心规则:返回包含ref的普通对象

// ✅ 推荐写法
export function useMouse() {
  const x = ref(0);
  const y = ref(0);
  return { x, y }; // 普通对象,里面是ref
}

// 组件里解构后,ref依然保持响应式
const { x, y } = useMouse();

为啥不用reactive返回?

// ❌ 不推荐
export function useMouse() {
  return reactive({
    x: 0,
    y: 0,
  });
}

// 解构后丢失响应性!
const { x, y } = useMouse(); // x和y变成普通数字了

reactive对象被解构后,每个属性就变成了普通值,跟原来的响应式对象断了联系。而ref不会——解构出来的ref本身就是一个响应式引用,不管你怎么传递都保持响应性。

往期文章归档
免费好用的热门在线工具

如果你想用对象属性的方式访问

有些人不喜欢在模板里写x.value(虽然<template>里会自动解包),更喜欢用mouse.x这种方式。那你可以用reactive把返回值包装一下:

const mouse = reactive(useMouse());
// mouse.x 自动解包了ref,直接就是值
// 而且响应性还在!因为reactive会自动解包ref
<template>鼠标位置:{{ mouse.x }}, {{ mouse.y }}</template>
flowchart TD
    A[Composable返回值] --> B{用什么包装?}
    B -->|推荐| C[普通对象 + ref]
    B -->|不推荐| D[reactive对象]
    C --> E[解构后保持响应式 ✅]
    D --> F[解构后丢失响应式 ❌]
    C --> G[需要对象访问? → reactive包装返回值]
    G --> H[reactive自动解包ref ✅]

五、一个完整的约定示例

把上面说的约定都合在一起,来写一个规范的Composable:

// composables/useUserList.js
import { ref, toValue, watchEffect } from "vue";

// 1. 函数名以use开头,驼峰命名
export function useUserList(url) {
  // 2. 用ref定义状态
  const users = ref([]);
  const loading = ref(false);
  const error = ref(null);

  // 3. 用toValue处理可能为ref/getter的参数
  // 4. 用watchEffect监听响应式依赖变化
  watchEffect(async () => {
    loading.value = true;
    error.value = null;

    try {
      const response = await fetch(toValue(url));
      users.value = await response.json();
    } catch (err) {
      error.value = err;
    } finally {
      loading.value = false;
    }
  });

  // 5. 返回包含ref的普通对象
  return { users, loading, error };
}

组件里用:

<script setup>
import { ref } from "vue";
import { useUserList } from "./composables/useUserList.js";

// 可以传字符串
const { users, loading, error } = useUserList("/api/users");

// 也可以传ref,URL变了会自动重新请求
const apiUrl = ref("/api/users");
const { users, loading, error } = useUserList(apiUrl);

// 还可以传getter
const { users, loading, error } = useUserList(
  () => `/api/users?page=${page.value}`,
);
</script>

<template>
  <div v-if="loading">加载中...</div>
  <div v-else-if="error">出错了:{{ error.message }}</div>
  <ul v-else>
    <li v-for="user in users" :key="user.id">{{ user.name }}</li>
  </ul>
</template>

课后 Quiz

问题 1

以下哪个Composable函数名不符合约定?

  • A. useDarkMode
  • B. getThemeColor
  • C. useScrollPosition
  • D. useLocalStorage

答案解析

B不符合约定。Composable函数名应该以use开头,getThemeColor看起来像普通工具函数。改成useThemeColor就对了。

问题 2

toValue()unref()的核心区别是什么?

答案解析

toValue()在遇到函数类型的参数时,会调用该函数并返回结果;而unref()遇到函数会原样返回,不会调用。对于ref和普通值,它俩的行为是一样的。所以当你需要兼容getter函数作为输入时,应该用toValue()

问题 3

为什么Composable推荐返回包含ref的普通对象,而不是reactive对象?

答案解析

因为reactive对象被解构后,每个属性会变成普通值,丢失响应性。而ref被解构后依然是ref,保持响应性。组件里通常会用解构的方式接收Composable的返回值,所以返回ref更安全。

常见报错解决方案

报错 1:toValue is not a function

错误场景

import { toValue } from "vue"; // 💥 报错

报错原因toValue()是Vue 3.3才新增的API,如果你的Vue版本低于3.3,就没有这个函数。

解决方案: 升级Vue到3.3以上,或者自己写一个简易版:

function toValue(value) {
  if (typeof value === "function") {
    return value();
  }
  return unref(value);
}

报错 2:Composable返回reactive对象后解构丢失响应性

错误场景

export function useCounter() {
  return reactive({
    count: 0,
    increment() {
      this.count++;
    },
  });
}

const { count, increment } = useCounter();
// count是普通数字,increment里的this也指向不对了

报错原因: reactive对象解构后属性变成普通值,而且方法中的this在解构后不再指向原对象。

解决方案: 改用ref + 普通对象的模式:

export function useCounter() {
  const count = ref(0);
  function increment() {
    count.value++;
  }
  return { count, increment }; // ✅ ref解构后保持响应式
}

报错 3:传了ref给Composable但变化时没有重新执行

错误场景

export function useFetch(url) {
  const data = ref(null);
  // 直接用了url,没有用toValue和watchEffect
  fetch(url)
    .then((res) => res.json())
    .then((json) => (data.value = json));
  return { data };
}

const url = ref("/api/users");
const { data } = useFetch(url); // 传了ref但没效果
url.value = "/api/posts"; // 不会重新请求

报错原因: 直接使用ref作为参数时,fetch拿到的是ref对象而不是它的值。而且没有用watchEffect来监听ref的变化,所以URL变了也不会重新请求。

解决方案: 用toValue()解析参数,用watchEffect()监听变化:

export function useFetch(url) {
  const data = ref(null);

  watchEffect(() => {
    fetch(toValue(url)) // ✅ toValue解析ref,watchEffect追踪依赖
      .then((res) => res.json())
      .then((json) => (data.value = json));
  });

  return { data };
}

参考链接