Vue 3组合式API实战:构建可复用的自定义Hook




Vue 3组合式API实战:构建可复用的自定义Hook


Vue 3组合式API实战:构建可复用的自定义Hook

Vue 3的组合式API彻底改变了我们组织组件逻辑的方式。与Options API相比,组合式API提供了更好的逻辑复用、类型支持和代码组织能力。今天,我们将深入探讨如何创建和使用自定义Hook来构建可复用的逻辑。

什么是自定义Hook?

自定义Hook是一个使用Vue组合式API的函数,它可以封装和复用有状态的逻辑。与React Hook类似,Vue的自定义Hook允许我们在多个组件之间共享逻辑。

关键概念:自定义Hook通常以”use”开头命名,这是社区约定俗成的规范。

基础示例:useMousePosition Hook

让我们从一个简单的例子开始:跟踪鼠标位置。

// useMousePosition.js
import { ref, onMounted, onUnmounted } from 'vue';

export function useMousePosition() {
    const x = ref(0);
    const y = ref(0);
    
    const updatePosition = (event) => {
        x.value = event.clientX;
        y.value = event.clientY;
    };
    
    onMounted(() => {
        window.addEventListener('mousemove', updatePosition);
    });
    
    onUnmounted(() => {
        window.removeEventListener('mousemove', updatePosition);
    });
    
    return { x, y };
}

// 在组件中使用
// Component.vue
import { useMousePosition } from './useMousePosition';

export default {
    setup() {
        const { x, y } = useMousePosition();
        
        return { x, y };
    }
};

高级Hook:useFetch数据获取

让我们创建一个更复杂的Hook来处理数据获取:

// useFetch.js
import { ref, computed, watch } from 'vue';

export function useFetch(url, options = {}) {
    const data = ref(null);
    const error = ref(null);
    const loading = ref(false);
    
    const fetchData = async () => {
        loading.value = true;
        error.value = null;
        
        try {
            const response = await fetch(url, options);
            
            if (!response.ok) {
                throw new Error(`HTTP error! status: ${response.status}`);
            }
            
            data.value = await response.json();
        } catch (err) {
            error.value = err.message;
            console.error('Fetch error:', err);
        } finally {
            loading.value = false;
        }
    };
    
    // 自动执行初次获取
    if (options.immediate !== false) {
        fetchData();
    }
    
    // 监听URL变化
    watch(() => url, (newUrl) => {
        if (newUrl) {
            fetchData();
        }
    });
    
    return {
        data,
        error,
        loading,
        fetchData,
        // 计算属性
        hasData: computed(() => data.value !== null),
        hasError: computed(() => error.value !== null)
    };
}

// 使用示例
// UserComponent.vue
import { useFetch } from './useFetch';

export default {
    props: ['userId'],
    setup(props) {
        const { 
            data: user, 
            loading, 
            error,
            fetchData 
        } = useFetch(
            () => `https://api.example.com/users/${props.userId}`,
            { immediate: true }
        );
        
        return { user, loading, error, fetchData };
    }
};

组合Hook:构建复杂逻辑

自定义Hook的真正威力在于它们可以相互组合:

// useLocalStorage.js
import { ref, watch } from 'vue';

export function useLocalStorage(key, defaultValue) {
    const data = ref(
        JSON.parse(localStorage.getItem(key)) || defaultValue
    );
    
    watch(data, (newValue) => {
        localStorage.setItem(key, JSON.stringify(newValue));
    }, { deep: true });
    
    return data;
}

// useAuth.js - 组合多个Hook
import { useFetch } from './useFetch';
import { useLocalStorage } from './useLocalStorage';

export function useAuth() {
    const token = useLocalStorage('auth_token', null);
    const user = useLocalStorage('auth_user', null);
    
    const { 
        data: loginResult, 
        loading: loginLoading, 
        error: loginError,
        fetchData: login 
    } = useFetch('https://api.example.com/login', {
        method: 'POST',
        immediate: false
    });
    
    // 监听登录结果
    watch(loginResult, (result) => {
        if (result && result.token) {
            token.value = result.token;
            user.value = result.user;
        }
    });
    
    const logout = () => {
        token.value = null;
        user.value = null;
        localStorage.removeItem('auth_token');
        localStorage.removeItem('auth_user');
    };
    
    const isAuthenticated = computed(() => !!token.value);
    
    return {
        token,
        user,
        login: (credentials) => login({
            body: JSON.stringify(credentials)
        }),
        loginLoading,
        loginError,
        logout,
        isAuthenticated
    };
}

TypeScript支持

使用TypeScript可以极大地提高自定义Hook的类型安全性:

// useCounter.ts
import { ref, computed } from 'vue';

interface UseCounterOptions {
    initialValue?: number;
    min?: number;
    max?: number;
}

interface UseCounterReturn {
    count: Ref;
    increment: () => void;
    decrement: () => void;
    reset: () => void;
    isMin: ComputedRef;
    isMax: ComputedRef;
}

export function useCounter(options: UseCounterOptions = {}): UseCounterReturn {
    const {
        initialValue = 0,
        min = -Infinity,
        max = Infinity
    } = options;
    
    const count = ref(initialValue);
    
    const increment = () => {
        if (count.value < max) {
            count.value++;
        }
    };
    
    const decrement = () => {
        if (count.value > min) {
            count.value--;
        }
    };
    
    const reset = () => {
        count.value = initialValue;
    };
    
    const isMin = computed(() => count.value <= min);
    const isMax = computed(() => count.value >= max);
    
    return {
        count,
        increment,
        decrement,
        reset,
        isMin,
        isMax
    };
}

最佳实践

最佳实践:

  1. 保持Hook的单一职责原则
  2. 使用TypeScript提供类型安全
  3. 为Hook提供清晰的文档和示例
  4. 考虑错误处理和边界情况
  5. 测试你的Hook

测试自定义Hook

// useCounter.test.js
import { renderHook, act } from '@testing-library/vue';
import { useCounter } from './useCounter';

describe('useCounter', () => {
    test('should initialize with default value', () => {
        const { result } = renderHook(() => useCounter());
        expect(result.current.count.value).toBe(0);
    });
    
    test('should increment count', () => {
        const { result } = renderHook(() => useCounter({ initialValue: 5 }));
        
        act(() => {
            result.current.increment();
        });
        
        expect(result.current.count.value).toBe(6);
    });
    
    test('should respect max limit', () => {
        const { result } = renderHook(() => 
            useCounter({ initialValue: 9, max: 10 })
        );
        
        act(() => {
            result.current.increment(); // 10
            result.current.increment(); // 应该还是10
        });
        
        expect(result.current.count.value).toBe(10);
        expect(result.current.isMax.value).toBe(true);
    });
});

总结

自定义Hook是Vue 3组合式API的核心特性之一,它们提供了:

  • 逻辑复用:在多个组件间共享业务逻辑
  • 代码组织:将相关逻辑组织在一起
  • 可测试性:独立的逻辑单元更容易测试
  • 类型安全:与TypeScript完美集成

通过创建和使用自定义Hook,你可以构建更加模块化、可维护和可测试的Vue应用。开始尝试将你的组件逻辑提取到自定义Hook中,你会发现代码质量得到显著提升。

在下一篇文章中,我们将探讨如何使用Vue 3的新特性构建高性能的动画效果。


© 版权声明
THE END
喜欢就支持一下吧
点赞8 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容