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

Vue 3 组合式 API 实战指南

Vue 3 引入了革命性的组合式 API(Composition API),它提供了更灵活、更强大的代码组织方式。本文将通过实例详细介绍组合式 API 的使用方法和最佳实践。

基础概念

1. Options API vs Composition API

// Options API
export default {
data() {
return {
count: 0,
todos: []
}
},
methods: {
increment() {
this.count++
},
addTodo(todo) {
this.todos.push(todo)
}
},
computed: {
doubleCount() {
return this.count * 2
}
}
}

// Composition API
import { ref, computed, methods } from 'vue'

export default {
setup() {
const count = ref(0)
const todos = ref([])

const increment = () => {
count.value++
}

const addTodo = (todo) => {
todos.value.push(todo)
}

const doubleCount = computed(() => count.value * 2)

return {
count,
todos,
increment,
addTodo,
doubleCount
}
}
}

2. 生命周期钩子

import { onMounted, onUpdated, onUnmounted } from 'vue'

export default {
setup() {
onMounted(() => {
console.log('组件已挂载')
})

onUpdated(() => {
console.log('组件已更新')
})

onUnmounted(() => {
console.log('组件已卸载')
})
}
}

3. 响应式系统

import { ref, reactive, computed, watch } from 'vue'

const counter = ref(0) // 基本类型响应式
const state = reactive({ // 对象类型响应式
count: 0,
name: 'Vue'
})

// 计算属性
const doubleCount = computed(() => counter.value * 2)

// 监听
watch(counter, (newVal, oldVal) => {
console.log(`Counter changed from ${oldVal} to ${newVal}`)
})

实战应用

1. 表单处理

<template>
<div>
<input v-model="form.name" placeholder="Name" />
<input v-model="form.email" placeholder="Email" type="email" />
<button @click="submitForm">Submit</button>
<div v-if="errors.name" class="error">{{ errors.name }}</div>
<div v-if="errors.email" class="error">{{ errors.email }}</div>
</div>
</template>

<script>
import { reactive, computed } from 'vue'

export default {
setup() {
const form = reactive({
name: '',
email: ''
})

const errors = reactive({
name: '',
email: ''
})

const validateForm = () => {
let isValid = true

if (!form.name.trim()) {
errors.name = 'Name is required'
isValid = false
} else {
errors.name = ''
}

if (!form.email.trim()) {
errors.email = 'Email is required'
isValid = false
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(form.email)) {
errors.email = 'Email is invalid'
isValid = false
} else {
errors.email = ''
}

return isValid
}

const submitForm = () => {
if (validateForm()) {
console.log('Form submitted:', form)
// 提交逻辑
}
}

return {
form,
errors,
submitForm
}
}
}
</script>

2. 列表操作

<template>
<div>
<input v-model="newTodo" placeholder="Add a new todo" />
<button @click="addTodo">Add</button>

<ul>
<li v-for="todo in todos" :key="todo.id">
{{ todo.text }}
<button @click="removeTodo(todo.id)">Remove</button>
</li>
</ul>

<div v-if="todos.length === 0">No todos yet</div>
</div>
</template>

<script>
import { ref } from 'vue'

export default {
setup() {
const todos = ref([
{ id: 1, text: 'Learn Vue 3', completed: false },
{ id: 2, text: 'Build an app', completed: false }
])

const newTodo = ref('')

const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
}

const removeTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id)
}

const toggleTodo = (id) => {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}

return {
todos,
newTodo,
addTodo,
removeTodo,
toggleTodo
}
}
}
</script>

3. API 调用

<template>
<div>
<div v-if="loading">Loading...</div>
<div v-else-if="error">{{ error }}</div>
<div v-else>
<div v-for="post in posts" :key="post.id">
<h3>{{ post.title }}</h3>
<p>{{ post.body }}</p>
</div>
</div>
</div>
</template>

<script>
import { ref, onMounted } from 'vue'

export default {
setup() {
const posts = ref([])
const loading = ref(false)
const error = ref(null)

const fetchPosts = async () => {
loading.value = true
try {
const response = await fetch('https://jsonplaceholder.typicode.com/posts')
if (!response.ok) {
throw new Error('Failed to fetch posts')
}
posts.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}

onMounted(() => {
fetchPosts()
})

return {
posts,
loading,
error
}
}
}
</script>

高级特性

1. 自定义 Composition 函数

// useCounter.js
import { ref, computed } 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)

return {
count,
increment,
decrement,
reset,
doubleCount
}
}

// 在组件中使用
import { useCounter } from './useCounter'

export default {
setup() {
const { count, increment, decrement, doubleCount } = useCounter(10)

return {
count,
increment,
decrement,
doubleCount
}
}
}

2. 自定义 Hook 组合

// useFetch.js
import { ref, onMounted } from 'vue'

export function useFetch(url) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)

const fetchData = async () => {
loading.value = true
try {
const response = await fetch(url)
if (!response.ok) {
throw new Error('Failed to fetch data')
}
data.value = await response.json()
} catch (err) {
error.value = err.message
} finally {
loading.value = false
}
}

onMounted(() => {
fetchData()
})

return {
data,
loading,
error,
refetch: fetchData
}
}

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

export function useLocalStorage(key, initialValue) {
const storedValue = localStorage.getItem(key)
const value = ref(storedValue ? JSON.parse(storedValue) : initialValue)

watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })

return value
}

// 使用组合
import { useFetch } from './useFetch'
import { useLocalStorage } from './useLocalStorage'
import { computed } from 'vue'

export default {
setup() {
const { data: posts, loading, error } = useFetch('https://jsonplaceholder.typicode.com/posts')
const favorites = useLocalStorage('favorites', [])

const favoritePosts = computed(() => {
return posts.value?.filter(post => favorites.value.includes(post.id)) || []
})

const toggleFavorite = (postId) => {
const index = favorites.value.indexOf(postId)
if (index === -1) {
favorites.value.push(postId)
} else {
favorites.value.splice(index, 1)
}
}

return {
posts,
loading,
error,
favoritePosts,
toggleFavorite
}
}
}

3. TypeScript 支持

// types.ts
export interface Todo {
id: number
text: string
completed: boolean
}

export interface FormState {
name: string
email: string
}

// 使用 TypeScript
import { ref, reactive } from 'vue'
import type { Ref, Reactive } from 'vue'
import { Todo, FormState } from './types'

export default {
setup() {
const todos: Ref<Todo[]> = ref([])
const form: Reactive<FormState> = reactive({
name: '',
email: ''
})

const addTodo = (todo: Todo) => {
todos.value.push(todo)
}

return {
todos,
form,
addTodo
}
}
}

最佳实践

1. 代码组织

// 按功能分组
function useAuth() {
// 认证相关逻辑
}

function useTodos() {
// 待办事项相关逻辑
}

function useUser() {
// 用户相关逻辑
}

export default {
setup() {
const { user } = useUser()
const { todos } = useTodos()
const { login } = useAuth()

return {
user,
todos,
login
}
}
}

2. 性能优化

import { shallowRef, shallowReactive } from 'vue'

// 对于大型对象,使用 shallowReactive
const largeObject = shallowReactive({
// 大型对象
})

// 对于不需要深度响应式的数据,使用 shallowRef
const largeArray = shallowRef([])

3. 错误处理

import { onErrorCaptured } from 'vue'

export default {
setup() {
const error = ref(null)

onErrorCaptured((err) => {
error.value = err
return false // 阻止错误继续传播
})

return {
error
}
}
}

4. 测试策略

// 组件测试
import { mount } from '@vue/test-utils'
import MyComponent from './MyComponent.vue'

describe('MyComponent', () => {
it('renders correctly', () => {
const wrapper = mount(MyComponent)
expect(wrapper.text()).toContain('Hello World')
})

it('handles user interaction', async () => {
const wrapper = mount(MyComponent)
await wrapper.find('button').trigger('click')
expect(wrapper.vm.count).toBe(1)
})
})

// Hook 测试
import { renderHook, act } from '@vue/test-utils'
import { useCounter } from './useCounter'

describe('useCounter', () => {
it('initializes with default value', () => {
const { result } = renderHook(() => useCounter())
expect(result.value.count.value).toBe(0)
})

it('increments count', () => {
const { result } = renderHook(() => useCounter())
act(() => {
result.value.increment()
})
expect(result.value.count.value).toBe(1)
})
})

迁移指南

1. 从 Options API 迁移

// Options API
export default {
data() {
return {
count: 0
}
},
methods: {
increment() {
this.count++
}
}
}

// Composition API
import { ref } from 'vue'

export default {
setup() {
const count = ref(0)

const increment = () => {
count.value++
}

return {
count,
increment
}
}
}

2. 生命周期映射

// beforeCreate -> setup()
export default {
setup() {
// beforeCreate 逻辑
}
}

// created -> setup()
export default {
setup() {
// created 逻辑
}
}

// beforeMount -> onMounted
import { onMounted } from 'vue'

export default {
setup() {
onMounted(() => {
// beforeMount 逻辑
})
}
}

// mounted -> onMounted
import { onMounted } from 'vue'

export default {
setup() {
onMounted(() => {
// mounted 逻辑
})
}
}

3. 计算属性和监听器

// computed -> computed
import { computed } from 'vue'

export default {
setup() {
const count = ref(0)
const doubleCount = computed(() => count.value * 2)

return {
count,
doubleCount
}
}
}

// watch -> watch
import { watch } from 'vue'

export default {
setup() {
const count = ref(0)

watch(count, (newVal, oldVal) => {
console.log(`Count changed from ${oldVal} to ${newVal}`)
})

return {
count
}
}
}

总结

Vue 3 组合式 API 提供了更灵活、更强大的代码组织方式,特别适合:

  1. 大型应用:更好地组织复杂逻辑
  2. 团队协作:逻辑复用更简单
  3. TypeScript 支持:更好的类型推断
  4. 代码复用:自定义组合函数

通过合理使用组合式 API,可以构建更加模块化、可维护的应用程序。建议在项目中逐步采用,并结合项目需求选择合适的组织方式。