𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

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来组织组件逻辑:

// 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('组件已挂载')
}
}

这种写法在小组件中很清晰,但在处理复杂逻辑时,会出现几个问题:

  1. 逻辑分散: 相关的逻辑被分散在不同的选项中
  2. 代码复用困难: 无法轻松地抽取和复用组件逻辑
  3. 类型推导困难: TypeScript支持不够友好
  4. 大型组件维护困难: 代码组织不够清晰

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的优势

  1. 逻辑组织: 相关逻辑放在一起
  2. 代码复用: 可以轻松抽取组合函数
  3. TypeScript支持: 更好的类型推导
  4. 灵活性强: 更灵活的逻辑组织方式

核心API详解

1. 响应式系统

ref和reactive

import { ref, reactive } from 'vue'

export default {
setup() {
// ref用于基本类型
const count = ref(0)
const name = ref('张三')

// reactive用于对象
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: '张三'
})

// 将对象的所有属性转换为ref
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 - 精确侦听
watch(count, (newVal, oldVal) => {
console.log(`count从${oldVal}变为${newVal}`)
})

// watch侦听多个值
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
console.log(`count从${oldCount}变为${newCount}, name从${oldName}变为${newName}`)
})

// watchEffect - 自动追踪依赖
watchEffect(() => {
console.log(`当前count: ${count.value}, name: ${name.value}`)
})

// 停止侦听
const stopWatch = watch(count, () => {
console.log('count变化了')
})

// 在适当的时候停止侦听
// stopWatch()

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:计数器组合函数

// composables/useCounter.js
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:表单验证组合函数

// composables/useFormValidation.js
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:本地存储组合函数

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

export function useLocalStorage(key, initialValue) {
const storedValue = ref(initialValue)

// 从localStorage读取
onMounted(() => {
try {
const item = window.localStorage.getItem(key)
if (item) {
storedValue.value = JSON.parse(item)
}
} catch (error) {
console.error('读取localStorage失败:', error)
}
})

// 写入localStorage
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请求组合函数

// composables/useApi.js
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">&times;</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. 组合函数的设计原则

  1. 单一职责: 每个组合函数应该专注一个特定功能
  2. 纯函数: 尽量保持组合函数的纯函数特性
  3. 可测试: 组合函数应该是可测试的
  4. 可组合: 组合函数可以相互组合

2. 命名约定

  1. 文件名: 使用use前缀,如useCounter.js
  2. 导出: 默认导出组合函数
  3. props: 使用明确的props定义

3. 错误处理

// composables/useAsync.js
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支持

// composables/useCounter.ts
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的强大特性,掌握它能让你编写出更加优雅和可维护的组件代码。如果觉得这篇文章对你有帮助,别忘了点赞收藏哦!