← 返回 Vue 分类

Composables 封装技巧

📅 2026-06-07 🏷️ Composables 👤 LoveQing

一、什么是 Composables

// Composables = 组合式函数
// 利用 Vue Composition API 封装可复用逻辑
// 命名规范:useXxx 开头

// ===== 基础结构 =====
// composables/useCounter.ts
import { ref, computed } from 'vue';

export function useCounter(initialValue = 0) {
  // 响应式状态
  const count = ref(initialValue);
  
  // 计算属性
  const double = computed(() => count.value * 2);
  
  // 方法
  const increment = () => count.value++;
  const decrement = () => count.value--;
  const reset = () => count.value = initialValue;
  
  // 返回状态和方法
  return { count, double, increment, decrement, reset };
}

// ===== 组件中使用 =====


二、常用 Composables 封装

// ===== 1. useToggle - 开关状态 =====
export function useToggle(initialValue = false) {
  const state = ref(initialValue);
  const toggle = () => state.value = !state.value;
  const setTrue = () => state.value = true;
  const setFalse = () => state.value = false;
  return { state, toggle, setTrue, setFalse };
}

// 使用
const { state: visible, toggle: toggleModal } = useToggle();


// ===== 2. useLocalStorage - 本地存储 =====
export function useLocalStorage(key: string, defaultValue: T) {
  const data = ref(defaultValue);

  // 初始化读取
  const stored = localStorage.getItem(key);
  if (stored) {
    data.value = JSON.parse(stored);
  }

  // 监听变化自动保存
  watch(data, (newVal) => {
    localStorage.setItem(key, JSON.stringify(newVal));
  }, { deep: true });

  return data;
}

// 使用
const settings = useLocalStorage('app-settings', { theme: 'light' });


// ===== 3. useDebounce - 防抖 =====
export function useDebounce(value: Ref, delay = 300) {
  const debouncedValue = ref(value.value) as Ref;
  let timer: NodeJS.Timeout;

  watch(value, (newVal) => {
    clearTimeout(timer);
    timer = setTimeout(() => {
      debouncedValue.value = newVal;
    }, delay);
  });

  return debouncedValue;
}

// 使用
const search = ref('');
const debouncedSearch = useDebounce(search, 500);
watch(debouncedSearch, doSearch);

三、异步请求封装

// ===== useFetch - 通用请求 =====
export function useFetch(url: string | Ref) {
  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(unref(url));
      data.value = await res.json();
    } catch (e) {
      error.value = e as Error;
    } finally {
      loading.value = false;
    }
  };

  // 自动执行
  if (isRef(url)) {
    watch(url, execute, { immediate: true });
  } else {
    execute();
  }

  return { data, loading, error, refresh: execute };
}

// 使用
const { data: users, loading, refresh } = useFetch('/api/users');


// ===== useAxios - 配合 Axios =====
import axios from 'axios';

export function useAxios(config: AxiosRequestConfig) {
  const data = ref(null);
  const loading = ref(false);

  const execute = async () => {
    loading.value = true;
    const res = await axios.request(config);
    data.value = res.data;
    loading.value = false;
  };

  execute();
  return { data, loading, execute };
}

四、DOM 相关 Composables

// ===== useEventListener - 事件监听 =====
export function useEventListener(
  target: EventTarget | Ref,
  event: string,
  callback: (e: Event) => void
) {
  const cleanup = () => {
    unref(target)?.removeEventListener(event, callback);
  };

  onMounted(() => {
    unref(target).addEventListener(event, callback);
  });

  onUnmounted(cleanup);
  return cleanup;
}

// 使用
useEventListener(window, 'resize', () => {
  console.log('窗口大小变化');
});


// ===== useMouse - 鼠标位置 =====
export function useMouse() {
  const x = ref(0);
  const y = ref(0);

  useEventListener(window, 'mousemove', (e: MouseEvent) => {
    x.value = e.clientX;
    y.value = e.clientY;
  });

  return { x, y };
}


// ===== useTitle - 页面标题 =====
export function useTitle(title: string | Ref) {
  watch(
    () => unref(title),
    (newTitle) => {
      document.title = newTitle;
    },
    { immediate: true }
  );
}

// 使用
useTitle('我的页面');

五、高级技巧

// ===== 1. 响应式参数 =====
export function useCount(initial: MaybeRef = 0) {
  // MaybeRef = T | Ref
  const count = ref(unref(initial));
  
  // 如果参数是 ref,监听变化
  if (isRef(initial)) {
    watch(initial, (val) => {
      count.value = val;
    });
  }
  
  return { count };
}

// 支持两种用法
useCount(10);           // 普通值
useCount(ref(10));      // ref


// ===== 2.  computed 可写 =====
export function useVModel(props: any, key: string, emit: any) {
  return computed({
    get() { return props[key]; },
    set(val) { emit(`update:${key}`, val); }
  });
}

// 组件中使用 v-model
const value = useVModel(props, 'modelValue', emit);


// ===== 3. 作用域状态共享 =====
const key = Symbol();

export function createUserStore() {
  const user = ref(null);
  
  function setUser(u) { user.value = u; }
  
  provide(key, { user, setUser });
  return { user, setUser };
}

export function useUserStore() {
  return inject(key);
}

// 父组件 provide
createUserStore();

// 子组件 inject
const { user } = useUserStore();

六、Composables 最佳实践

// ✅ 命名:use 开头,小驼峰
useCounter, useLocalStorage, useMouse

// ✅ 纯函数:无副作用,相同输入相同输出
// ❌ 不要在 composable 里直接修改全局状态

// ✅ 返回对象:明确返回值,方便解构
return { data, loading, error }

// ✅ 泛型:支持 TypeScript 类型
function useFetch(url: string) { ... }

// ✅ 自动清理:定时器、监听器、事件
onUnmounted(() => {
  clearTimeout(timer);
});

// ✅ 单一职责:一个 composable 只做一件事
// ❌ 不要把所有逻辑都塞到一个 composable

Composables vs Mixins

特性ComposablesMixins
类型安全✅ TypeScript 友好❌ 类型丢失
来源清晰✅ 明确导入❌ 隐式混入
命名冲突✅ 重命名解决❌ 自动覆盖
逻辑复用✅ 灵活组合⚠️ 继承式
参数传递✅ 函数参数❌ 配置对象