Vue 3 Composition API实战 - 打造可复用的组件系统
作为一名Vue.js开发者,从Vue 2升级到Vue 3后,最令人兴奋的变化之一就是Composition API的出现。作为一个在Vue领域深耕多年的开发者,我必须说Composition API彻底改变了我们编写组件的方式。它不仅解决了Options API在复杂组件中的逻辑组织问题,还让我们能够更好地复用代码逻辑。今天,我就来和大家深入探讨Vue 3 Composition API的实战应用,看看如何用它来构建真正可复用的组件系统。
为什么需要Composition API?
在Vue 2中,我们使用Options API来组织组件逻辑:
export default { data() { return { count: 0, name: '张三' } }, computed: { doubleCount() { return this.count * 2 } }, methods: { increment() { this.count++ }, greet() { return `你好,${this.name}!` } }, mounted() { console.log('组件已挂载') } }
|
这种写法在小组件中很清晰,但在处理复杂逻辑时,会出现几个问题:
- 逻辑分散: 相关的逻辑被分散在不同的选项中
- 代码复用困难: 无法轻松地抽取和复用组件逻辑
- 类型推导困难: TypeScript支持不够友好
- 大型组件维护困难: 代码组织不够清晰
Composition API通过引入setup()函数和Composition函数,解决了这些问题。
Composition API基础
setup函数
setup()函数是Composition API的核心入口,它在组件创建之前执行,返回的对象会暴露给模板使用。
import { ref, computed } from 'vue'
export default { setup() { const count = ref(0) const name = ref('张三') const doubleCount = computed(() => count.value * 2) const increment = () => { count.value++ } const greet = () => { return `你好,${name.value}!` } return { count, name, doubleCount, increment, greet } } }
|
组合式API的优势
- 逻辑组织: 相关逻辑放在一起
- 代码复用: 可以轻松抽取组合函数
- TypeScript支持: 更好的类型推导
- 灵活性强: 更灵活的逻辑组织方式
核心API详解
1. 响应式系统
ref和reactive
import { ref, reactive } from 'vue'
export default { setup() { const count = ref(0) const name = ref('张三') const user = reactive({ firstName: '张', lastName: '三', age: 25 }) return { count, name, user } } }
|
在模板中使用时,ref需要.value,但模板中会自动解包:
<template> <div> <p>计数: {{ count }}</p> <p>姓名: {{ name }}</p> <p>用户: {{ user.firstName }} {{ user.lastName }}</p> <button @click="count++">增加</button> </div> </template>
|
toRef和toRefs
import { reactive, toRefs } from 'vue'
export default { setup() { const state = reactive({ count: 0, name: '张三' }) const stateRefs = toRefs(state) const countRef = toRef(state, 'count') return { ...stateRefs, countRef } } }
|
2. 计算属性和侦听器
computed
import { ref, computed } from 'vue'
export default { setup() { const firstName = ref('张') const lastName = ref('三') const fullName = computed(() => { return `${firstName.value} ${lastName.value}` }) const fullNameWithSetter = computed({ get: () => `${firstName.value} ${lastName.value}`, set: (newValue) => { const names = newValue.split(' ') firstName.value = names[0] lastName.value = names[1] || '' } }) return { firstName, lastName, fullName, fullNameWithSetter } } }
|
watch和watchEffect
import { ref, watch, watchEffect } from 'vue'
export default { setup() { const count = ref(0) const name = ref('张三') watch(count, (newVal, oldVal) => { console.log(`count从${oldVal}变为${newVal}`) }) watch([count, name], ([newCount, newName], [oldCount, oldName]) => { console.log(`count从${oldCount}变为${newCount}, name从${oldName}变为${newName}`) }) watchEffect(() => { console.log(`当前count: ${count.value}, name: ${name.value}`) }) const stopWatch = watch(count, () => { console.log('count变化了') }) return { count, name } } }
|
3. 生命周期钩子
Composition API提供了对应的生命周期钩子:
import { onMounted, onUpdated, onUnmounted, onBeforeMount, onBeforeUpdate, onErrorCaptured } from 'vue'
export default { setup() { onBeforeMount(() => { console.log('挂载之前') }) onMounted(() => { console.log('挂载完成') }) onBeforeUpdate(() => { console.log('更新之前') }) onUpdated(() => { console.log('更新完成') }) onUnmounted(() => { console.log('组件卸载') }) onErrorCaptured((err, instance, info) => { console.error('错误捕获:', err, info) return false }) const loading = ref(false) onMounted(async () => { loading.value = true try { const module = await import('./AsyncComponent.vue') console.log('组件加载完成') } catch (error) { console.error('组件加载失败:', error) } finally { loading.value = false } }) return { loading } } }
|
实战:构建可复用的组件系统
1. 组合函数(Composables)的设计
组合函数是Composition API的精髓,它允许我们将可复用的逻辑抽取出来。
示例1:计数器组合函数
import { ref, computed, watch } from 'vue'
export function useCounter(initialValue = 0) { const count = ref(initialValue) const increment = () => { count.value++ } const decrement = () => { count.value-- } const reset = () => { count.value = initialValue } const doubleCount = computed(() => count.value * 2) watch(count, (newVal) => { console.log(`计数变为: ${newVal}`) }) return { count, doubleCount, increment, decrement, reset } }
|
使用方式:
import { useCounter } from './composables/useCounter'
export default { setup() { const { count, doubleCount, increment, decrement, reset } = useCounter(10) return { count, doubleCount, increment, decrement, reset } } }
|
示例2:表单验证组合函数
import { ref, computed } from 'vue'
export function useFormValidation(rules) { const formData = ref({}) const errors = ref({}) const isValid = ref(false) const setFieldValue = (field, value) => { formData.value[field] = value validateField(field, value) } const validateField = (field, value) => { const rule = rules[field] if (!rule) return true if (rule.required && (!value || value.toString().trim() === '')) { errors.value[field] = `${field}是必填项` return false } if (rule.minLength && value.length < rule.minLength) { errors.value[field] = `${field}最少需要${rule.minLength}个字符` return false } if (rule.pattern && !rule.pattern.test(value)) { errors.value[field] = rule.message || `${field}格式不正确` return false } delete errors.value[field] return true } const validateForm = () => { let valid = true Object.keys(rules).forEach(field => { const value = formData.value[field] if (!validateField(field, value)) { valid = false } }) isValid.value = valid return valid } const resetForm = () => { formData.value = {} errors.value = {} isValid.value = false } const hasError = computed(() => Object.keys(errors.value).length > 0) return { formData, errors, isValid, setFieldValue, validateForm, resetForm, hasError } }
|
使用方式:
import { useFormValidation } from './composables/useFormValidation'
export default { setup() { const rules = { username: { required: true, minLength: 3, message: '用户名至少3个字符' }, email: { required: true, pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/, message: '邮箱格式不正确' }, password: { required: true, minLength: 6, message: '密码至少6个字符' } } const { formData, errors, isValid, setFieldValue, validateForm, resetForm, hasError } = useFormValidation(rules) const handleSubmit = () => { if (validateForm()) { console.log('表单提交:', formData.value) } } return { formData, errors, isValid, setFieldValue, handleSubmit, resetForm, hasError } } }
|
2. 高级组合函数
示例3:本地存储组合函数
import { ref, watch, onMounted } from 'vue'
export function useLocalStorage(key, initialValue) { const storedValue = ref(initialValue) onMounted(() => { try { const item = window.localStorage.getItem(key) if (item) { storedValue.value = JSON.parse(item) } } catch (error) { console.error('读取localStorage失败:', error) } }) const setValue = (value) => { try { const jsonValue = JSON.stringify(value) window.localStorage.setItem(key, jsonValue) storedValue.value = value } catch (error) { console.error('写入localStorage失败:', error) } } watch(storedValue, (newVal) => { setValue(newVal) }, { deep: true }) const remove = () => { window.localStorage.removeItem(key) storedValue.value = initialValue } return { value: storedValue, setValue, remove } }
|
使用方式:
import { useLocalStorage } from './composables/useLocalStorage'
export default { setup() { const { value: theme, setValue: setTheme } = useLocalStorage('theme', 'light') const toggleTheme = () => { setTheme(theme.value === 'light' ? 'dark' : 'light') } return { theme, toggleTheme } } }
|
示例4:API请求组合函数
import { ref, onMounted } from 'vue'
export function useApi(url, options = {}) { const data = ref(null) const loading = ref(false) const error = ref(null) const fetchData = async (params = {}) => { loading.value = true error.value = null try { const queryString = new URLSearchParams(params).toString() const fullUrl = queryString ? `${url}?${queryString}` : url const response = await fetch(fullUrl, { method: 'GET', headers: { 'Content-Type': 'application/json', ...options.headers }, ...options }) if (!response.ok) { throw new Error(`HTTP错误! 状态: ${response.status}`) } data.value = await response.json() } catch (err) { error.value = err.message console.error('API请求失败:', err) } finally { loading.value = false } } onMounted(() => { fetchData() }) return { data, loading, error, fetchData } }
|
使用方式:
import { useApi } from './composables/useApi'
export default { setup() { const { data, loading, error, fetchData } = useApi('https://api.example.com/users') const refreshData = () => { fetchData({ page: 1, limit: 10 }) } return { data, loading, error, refreshData } } }
|
3. 组件复用示例
示例5:可复用的模态框组件
<!-- components/BaseModal.vue --> <template> <transition name="modal"> <div v-if="show" class="modal-overlay" @click="closeOnOverlay && close()"> <div class="modal-content" @click.stop> <div class="modal-header"> <h3>{{ title }}</h3> <button class="close-btn" @click="close">×</button> </div> <div class="modal-body"> <slot></slot> </div> <div class="modal-footer"> <slot name="footer"> <button class="btn btn-primary" @click="confirm"> {{ confirmText }} </button> <button class="btn btn-secondary" @click="close"> {{ cancelText }} </button> </slot> </div> </div> </div> </transition> </template>
<script> import { ref } from 'vue'
export default { props: { show: { type: Boolean, default: false }, title: { type: String, default: '确认' }, confirmText: { type: String, default: '确定' }, cancelText: { type: String, default: '取消' }, closeOnOverlay: { type: Boolean, default: true } }, emits: ['close', 'confirm'], setup(props, { emit }) { const close = () => { emit('close') } const confirm = () => { emit('confirm') } return { close, confirm } } } </script>
<style scoped> .modal-overlay { position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0, 0, 0, 0.5); display: flex; align-items: center; justify-content: center; z-index: 1000; }
.modal-content { background: white; border-radius: 8px; max-width: 500px; width: 90%; max-height: 90vh; overflow: auto; }
.modal-header { padding: 20px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
.modal-body { padding: 20px; }
.modal-footer { padding: 20px; border-top: 1px solid #eee; display: flex; justify-content: flex-end; gap: 10px; }
.close-btn { background: none; border: none; font-size: 24px; cursor: pointer; color: #666; }
.btn { padding: 8px 16px; border: none; border-radius: 4px; cursor: pointer; }
.btn-primary { background: #007bff; color: white; }
.btn-secondary { background: #6c757d; color: white; }
.modal-enter-active, .modal-leave-active { transition: opacity 0.3s; }
.modal-enter-from, .modal-leave-to { opacity: 0; } </style>
|
使用方式:
<!-- 使用BaseModal --> <template> <div> <button @click="showModal = true">打开模态框</button> <BaseModal :show="showModal" title="确认删除" @close="showModal = false" @confirm="handleDelete" > <p>确定要删除这个项目吗?此操作不可撤销。</p> </BaseModal> </div> </template>
<script> import { ref } from 'vue' import BaseModal from './BaseModal.vue'
export default { components: { BaseModal }, setup() { const showModal = ref(false) const handleDelete = () => { // 删除逻辑 console.log('删除项目') showModal.value = false } return { showModal, handleDelete } } } </script>
|
最佳实践
1. 组合函数的设计原则
- 单一职责: 每个组合函数应该专注一个特定功能
- 纯函数: 尽量保持组合函数的纯函数特性
- 可测试: 组合函数应该是可测试的
- 可组合: 组合函数可以相互组合
2. 命名约定
- 文件名: 使用
use前缀,如useCounter.js - 导出: 默认导出组合函数
- props: 使用明确的props定义
3. 错误处理
import { ref } from 'vue'
export function useAsync(fn) { const data = ref(null) const loading = ref(false) const error = ref(null) const execute = async (...args) => { loading.value = true error.value = null try { data.value = await fn(...args) } catch (err) { error.value = err throw err } finally { loading.value = false } } return { data, loading, error, execute } }
|
4. TypeScript支持
import { ref, computed, watch } from 'vue'
export interface CounterOptions { initialValue?: number max?: number min?: number }
export function useCounter(options: CounterOptions = {}) { const { initialValue = 0, max, min } = options const count = ref(initialValue) const increment = () => { if (max !== undefined && count.value >= max) return count.value++ } const decrement = () => { if (min !== undefined && count.value <= min) return count.value-- } const reset = () => { count.value = initialValue } const doubleCount = computed(() => count.value * 2) watch(count, (newVal) => { if (max !== undefined && newVal > max) { count.value = max } if (min !== undefined && newVal < min) { count.value = min } }) return { count, doubleCount, increment, decrement, reset } }
|
总结
Vue 3 Composition API为我们提供了一种全新的组件逻辑组织方式。通过组合函数的设计,我们可以轻松地构建可复用的组件系统,提高代码的可维护性和可测试性。
记住,Composition API不是为了替换Options API,而是为了更好地组织复杂的组件逻辑。在实际项目中,可以根据具体需求选择合适的方式来编写组件。
希望这篇文章能够帮助你更好地理解和使用Composition API。如果你有任何问题或者有更好的实践经验,欢迎在评论区分享!
Composition API是Vue 3的强大特性,掌握它能让你编写出更加优雅和可维护的组件代码。如果觉得这篇文章对你有帮助,别忘了点赞收藏哦!
Vue 3 Composition API实战 - 打造可复用的组件系统