Skip to content

composables.js

文件信息

  • 📄 原文件:02_composables.js
  • 🔤 语言:javascript

Vue 3 Composables(组合式函数) Composables 是使用 Composition API 封装和复用有状态逻辑的函数。 类似于 React 的自定义 Hooks。 命名约定:以 "use" 开头,如 useCounter、useFetch

完整代码

javascript
/**
 * ============================================================
 *                Vue 3 Composables(组合式函数)
 * ============================================================
 * Composables 是使用 Composition API 封装和复用有状态逻辑的函数。
 * 类似于 React 的自定义 Hooks。
 *
 * 命名约定:以 "use" 开头,如 useCounter、useFetch
 * ============================================================
 */

import {
    ref,
    reactive,
    computed,
    watch,
    watchEffect,
    onMounted,
    onUnmounted,
    toValue,
    readonly,
} from 'vue';


// ============================================================
//                    1. useCounter - 计数器
// ============================================================

/**
 * 计数器 Composable
 *
 * 封装计数器的状态和逻辑
 *
 * @param {number} initialValue - 初始值,默认为 0
 * @returns {Object} 计数器状态和方法
 *
 * @example
 * ```vue
 * <script setup>
 * const { count, increment, decrement, reset } = useCounter(10);
 * </script>
 *
 * <template>
 *   <p>{{ count }}</p>
 *   <button @click="increment">+</button>
 *   <button @click="decrement">-</button>
 *   <button @click="reset">重置</button>
 * </template>
 * ```
 */
export function useCounter(initialValue = 0) {
    // 响应式状态
    const count = ref(initialValue);

    // 方法
    function increment() {
        count.value++;
    }

    function decrement() {
        count.value--;
    }

    function reset() {
        count.value = initialValue;
    }

    function set(value) {
        count.value = value;
    }

    // 计算属性
    const doubleCount = computed(() => count.value * 2);
    const isPositive = computed(() => count.value > 0);

    // 返回状态和方法
    return {
        count: readonly(count),  // 只读,防止外部直接修改
        doubleCount,
        isPositive,
        increment,
        decrement,
        reset,
        set,
    };
}


// ============================================================
//                    2. useFetch - 数据获取
// ============================================================

/**
 * 数据获取 Composable
 *
 * 封装异步数据获取逻辑,包括加载状态和错误处理
 *
 * @param {string | Ref<string> | () => string} url - 请求 URL
 * @param {Object} options - 请求选项
 * @returns {Object} 数据、加载状态、错误信息和方法
 *
 * @example
 * ```vue
 * <script setup>
 * const { data, loading, error, execute } = useFetch('/api/users');
 * </script>
 *
 * <template>
 *   <div v-if="loading">加载中...</div>
 *   <div v-else-if="error">{{ error }}</div>
 *   <div v-else>{{ data }}</div>
 * </template>
 * ```
 */
export function useFetch(url, options = {}) {
    const {
        immediate = true,      // 是否立即执行
        refetch = false,       // URL 变化时是否重新获取
        initialData = null,    // 初始数据
    } = options;

    // 状态
    const data = ref(initialData);
    const loading = ref(false);
    const error = ref(null);

    // 获取数据
    async function execute() {
        // 解析 URL(支持 ref 和函数)
        const resolvedUrl = toValue(url);

        if (!resolvedUrl) return;

        loading.value = true;
        error.value = null;

        try {
            const response = await fetch(resolvedUrl);

            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }

            data.value = await response.json();
        } catch (err) {
            error.value = err.message || '请求失败';
        } finally {
            loading.value = false;
        }
    }

    // 立即执行
    if (immediate) {
        execute();
    }

    // URL 变化时重新获取
    if (refetch) {
        watch(
            () => toValue(url),
            () => execute(),
            { immediate: false }
        );
    }

    return {
        data,
        loading,
        error,
        execute,  // 手动重新获取
    };
}


// ============================================================
//                    3. useLocalStorage - 本地存储
// ============================================================

/**
 * 本地存储 Composable
 *
 * 将 ref 的值同步到 localStorage
 *
 * @param {string} key - 存储的键名
 * @param {any} defaultValue - 默认值
 * @returns {Ref} 响应式的值
 *
 * @example
 * ```vue
 * <script setup>
 * const theme = useLocalStorage('theme', 'light');
 * </script>
 *
 * <template>
 *   <select v-model="theme">
 *     <option value="light">浅色</option>
 *     <option value="dark">深色</option>
 *   </select>
 * </template>
 * ```
 */
export function useLocalStorage(key, defaultValue) {
    // 从 localStorage 读取初始值
    const storedValue = localStorage.getItem(key);
    const initialValue = storedValue ? JSON.parse(storedValue) : defaultValue;

    // 创建响应式引用
    const data = ref(initialValue);

    // 监听变化,同步到 localStorage
    watch(
        data,
        (newValue) => {
            if (newValue === null || newValue === undefined) {
                localStorage.removeItem(key);
            } else {
                localStorage.setItem(key, JSON.stringify(newValue));
            }
        },
        { deep: true }
    );

    return data;
}


// ============================================================
//                    4. useEventListener - 事件监听
// ============================================================

/**
 * 事件监听 Composable
 *
 * 自动在组件卸载时移除事件监听
 *
 * @param {EventTarget} target - 目标元素
 * @param {string} event - 事件名
 * @param {Function} handler - 事件处理函数
 * @param {Object} options - addEventListener 选项
 *
 * @example
 * ```vue
 * <script setup>
 * useEventListener(window, 'resize', () => {
 *   console.log('窗口大小变化');
 * });
 * </script>
 * ```
 */
export function useEventListener(target, event, handler, options = {}) {
    // 支持 ref 作为 target
    const targetElement = toValue(target);

    onMounted(() => {
        targetElement?.addEventListener(event, handler, options);
    });

    onUnmounted(() => {
        targetElement?.removeEventListener(event, handler, options);
    });
}


// ============================================================
//                    5. useWindowSize - 窗口尺寸
// ============================================================

/**
 * 窗口尺寸 Composable
 *
 * 响应式获取窗口尺寸
 *
 * @returns {Object} 窗口宽度和高度
 *
 * @example
 * ```vue
 * <script setup>
 * const { width, height } = useWindowSize();
 * </script>
 *
 * <template>
 *   <p>窗口: {{ width }} x {{ height }}</p>
 * </template>
 * ```
 */
export function useWindowSize() {
    const width = ref(window.innerWidth);
    const height = ref(window.innerHeight);

    function update() {
        width.value = window.innerWidth;
        height.value = window.innerHeight;
    }

    onMounted(() => {
        window.addEventListener('resize', update);
    });

    onUnmounted(() => {
        window.removeEventListener('resize', update);
    });

    return {
        width: readonly(width),
        height: readonly(height),
    };
}


// ============================================================
//                    6. useMouse - 鼠标位置
// ============================================================

/**
 * 鼠标位置 Composable
 *
 * 响应式追踪鼠标位置
 *
 * @returns {Object} 鼠标 x, y 坐标
 *
 * @example
 * ```vue
 * <script setup>
 * const { x, y } = useMouse();
 * </script>
 *
 * <template>
 *   <p>鼠标位置: ({{ x }}, {{ y }})</p>
 * </template>
 * ```
 */
export function useMouse() {
    const x = ref(0);
    const y = ref(0);

    function update(event) {
        x.value = event.clientX;
        y.value = event.clientY;
    }

    onMounted(() => {
        window.addEventListener('mousemove', update);
    });

    onUnmounted(() => {
        window.removeEventListener('mousemove', update);
    });

    return { x: readonly(x), y: readonly(y) };
}


// ============================================================
//                    7. useDebounce - 防抖
// ============================================================

/**
 * 防抖 Composable
 *
 * 创建一个防抖的 ref
 *
 * @param {any} value - 初始值
 * @param {number} delay - 延迟时间(毫秒)
 * @returns {Object} 原始值和防抖后的值
 *
 * @example
 * ```vue
 * <script setup>
 * const { value, debouncedValue } = useDebounce('', 300);
 * </script>
 *
 * <template>
 *   <input v-model="value" />
 *   <p>防抖值: {{ debouncedValue }}</p>
 * </template>
 * ```
 */
export function useDebounce(initialValue, delay = 300) {
    const value = ref(initialValue);
    const debouncedValue = ref(initialValue);

    let timeoutId = null;

    watch(value, (newValue) => {
        // 清除之前的定时器
        if (timeoutId) {
            clearTimeout(timeoutId);
        }

        // 设置新的定时器
        timeoutId = setTimeout(() => {
            debouncedValue.value = newValue;
        }, delay);
    });

    // 清理定时器
    onUnmounted(() => {
        if (timeoutId) {
            clearTimeout(timeoutId);
        }
    });

    return {
        value,
        debouncedValue: readonly(debouncedValue),
    };
}


// ============================================================
//                    8. useToggle - 切换状态
// ============================================================

/**
 * 切换状态 Composable
 *
 * @param {boolean} initialValue - 初始值
 * @returns {Array} [状态, 切换函数, 设置函数]
 *
 * @example
 * ```vue
 * <script setup>
 * const [isOpen, toggle, setOpen] = useToggle(false);
 * </script>
 *
 * <template>
 *   <button @click="toggle">切换</button>
 *   <div v-if="isOpen">内容</div>
 * </template>
 * ```
 */
export function useToggle(initialValue = false) {
    const state = ref(initialValue);

    function toggle() {
        state.value = !state.value;
    }

    function set(value) {
        state.value = value;
    }

    return [readonly(state), toggle, set];
}


// ============================================================
//                    9. useAsync - 异步状态
// ============================================================

/**
 * 异步状态 Composable
 *
 * 管理异步操作的状态
 *
 * @param {Function} asyncFn - 异步函数
 * @param {Object} options - 选项
 * @returns {Object} 状态和执行函数
 *
 * @example
 * ```vue
 * <script setup>
 * const { execute, loading, error, data } = useAsync(async () => {
 *   const response = await fetch('/api/data');
 *   return response.json();
 * });
 * </script>
 * ```
 */
export function useAsync(asyncFn, options = {}) {
    const { immediate = false, initialData = null } = options;

    const data = ref(initialData);
    const loading = ref(false);
    const error = ref(null);

    async function execute(...args) {
        loading.value = true;
        error.value = null;

        try {
            data.value = await asyncFn(...args);
            return data.value;
        } catch (err) {
            error.value = err;
            throw err;
        } finally {
            loading.value = false;
        }
    }

    if (immediate) {
        execute();
    }

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


// ============================================================
//                    10. useForm - 表单处理
// ============================================================

/**
 * 表单处理 Composable
 *
 * @param {Object} initialValues - 初始表单值
 * @param {Object} validationRules - 验证规则
 * @returns {Object} 表单状态和方法
 *
 * @example
 * ```vue
 * <script setup>
 * const { values, errors, handleSubmit, resetForm } = useForm(
 *   { email: '', password: '' },
 *   {
 *     email: (v) => /.+@.+/.test(v) || '邮箱格式不正确',
 *     password: (v) => v.length >= 6 || '密码至少6位',
 *   }
 * );
 *
 * const onSubmit = handleSubmit((values) => {
 *   console.log('提交:', values);
 * });
 * </script>
 * ```
 */
export function useForm(initialValues, validationRules = {}) {
    // 表单值
    const values = reactive({ ...initialValues });

    // 错误信息
    const errors = reactive({});

    // 是否被修改
    const isDirty = ref(false);

    // 是否正在提交
    const isSubmitting = ref(false);

    // 验证单个字段
    function validateField(field) {
        const rule = validationRules[field];
        if (!rule) return true;

        const result = rule(values[field]);
        if (result === true) {
            delete errors[field];
            return true;
        } else {
            errors[field] = result;
            return false;
        }
    }

    // 验证所有字段
    function validate() {
        let isValid = true;
        for (const field in validationRules) {
            if (!validateField(field)) {
                isValid = false;
            }
        }
        return isValid;
    }

    // 处理提交
    function handleSubmit(onSubmit) {
        return async (event) => {
            event?.preventDefault();

            if (!validate()) return;

            isSubmitting.value = true;
            try {
                await onSubmit(values);
            } finally {
                isSubmitting.value = false;
            }
        };
    }

    // 重置表单
    function resetForm() {
        Object.assign(values, initialValues);
        Object.keys(errors).forEach((key) => delete errors[key]);
        isDirty.value = false;
    }

    // 设置字段值
    function setFieldValue(field, value) {
        values[field] = value;
        isDirty.value = true;
    }

    // 监听值变化,标记为已修改
    watch(
        () => values,
        () => {
            isDirty.value = true;
        },
        { deep: true }
    );

    return {
        values,
        errors,
        isDirty,
        isSubmitting,
        validateField,
        validate,
        handleSubmit,
        resetForm,
        setFieldValue,
    };
}


// ============================================================
//                    导出所有 Composables
// ============================================================

export default {
    useCounter,
    useFetch,
    useLocalStorage,
    useEventListener,
    useWindowSize,
    useMouse,
    useDebounce,
    useToggle,
    useAsync,
    useForm,
};

💬 讨论

使用 GitHub 账号登录后即可参与讨论

基于 MIT 许可发布