← 返回 Vue 分类
Composables 封装技巧
一、什么是 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 };
}
// ===== 组件中使用 =====
Count: {{ count }}
Double: {{ double }}
二、常用 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
| 特性 | Composables | Mixins |
|---|---|---|
| 类型安全 | ✅ TypeScript 友好 | ❌ 类型丢失 |
| 来源清晰 | ✅ 明确导入 | ❌ 隐式混入 |
| 命名冲突 | ✅ 重命名解决 | ❌ 自动覆盖 |
| 逻辑复用 | ✅ 灵活组合 | ⚠️ 继承式 |
| 参数传递 | ✅ 函数参数 | ❌ 配置对象 |