Vue 3 组合式 API 实战:从 Options API 迁移到 Composition API 的完整指南
引言
Vue 3 的 Composition API 已经发布多年,但许多团队仍在使用 Options API。这不是因为 Composition API 不好,而是因为迁移成本和认知负担。本文将从实战角度出发,带你理解为什么 Composition API 是更好的选择,以及如何平滑迁移。
为什么需要 Composition API?
Options API 的问题
在大型组件中,Options API 有一个致命问题:逻辑碎片化。
<script>
export default {
data() {
return {
searchQuery: '',
results: [],
loading: false,
error: null,
page: 1,
totalPages: 0
}
},
computed: {
filteredResults() {
return this.results.filter(item =>
item.name.includes(this.searchQuery)
)
}
},
watch: {
searchQuery: 'debouncedSearch',
page: 'fetchData'
},
methods: {
async fetchData() {
this.loading = true
this.error = null
try {
const res = await fetch(`/api/search?q=${this.searchQuery}&page=${this.page}`)
const data = await res.json()
this.results = data.items
this.totalPages = data.totalPages
} catch (e) {
this.error = e.message
} finally {
this.loading = false
}
},
debouncedSearch() {
// 防抖逻辑
},
nextPage() {
if (this.page < this.totalPages) {
this.page++
}
}
},
mounted() {
this.fetchData()
}
}
</script>
这个组件的问题很明显:
- 搜索相关的逻辑(searchQuery、debouncedSearch、filteredResults)分散在 data、methods、computed、watch 中
- 分页逻辑(page、totalPages、nextPage)也分散各处
- 数据获取逻辑(loading、error、fetchData)同样分散
当组件变大时,你需要不断上下滚动来理解一个功能的完整逻辑。
Composition API 的解决方案
Composition API 允许我们将相关逻辑组合在一起:
<script setup>
import { ref, computed, watch, onMounted } from 'vue'
import { useDebounceFn } from '@vueuse/core'
// ===== 搜索功能 =====
const searchQuery = ref('')
const debouncedSearch = useDebounceFn(() => {
page.value = 1
fetchData()
}, 300)
watch(searchQuery, debouncedSearch)
// ===== 分页功能 =====
const page = ref(1)
const totalPages = ref(0)
const nextPage = () => {
if (page.value < totalPages.value) {
page.value++
}
}
// ===== 数据获取 =====
const results = ref([])
const loading = ref(false)
const error = ref(null)
const filteredResults = computed(() =>
results.value.filter(item =>
item.name.includes(searchQuery.value)
)
)
async function fetchData() {
loading.value = true
error.value = null
try {
const res = await fetch(`/api/search?q=${searchQuery.value}&page=${page.value}`)
const data = await res.json()
results.value = data.items
totalPages.value = data.totalPages
} catch (e) {
error.value = e.message
} finally {
loading.value = false
}
}
watch(page, fetchData)
onMounted(fetchData)
</script>
现在,相关代码都在一起。你可以一眼看出每个功能的完整逻辑。
实战:提取可复用逻辑
Composition API 的真正威力在于提取可复用的组合式函数(Composables)。
示例:useSearch
// composables/useSearch.ts
import { ref, computed } from 'vue'
import { useDebounceFn } from '@vueuse/core'
interface UseSearchOptions<T> {
searchFn: (query: string) => Promise<T[]>
debounceMs?: number
}
export function useSearch<T>(options: UseSearchOptions<T>) {
const { searchFn, debounceMs = 300 } = options
const query = ref('')
const results = ref<T[]>([])
const loading = ref(false)
const error = ref<string | null>(null)
const debouncedSearch = useDebounceFn(async () => {
if (!query.value.trim()) {
results.value = []
return
}
loading.value = true
error.value = null
try {
results.value = await searchFn(query.value)
} catch (e) {
error.value = e instanceof Error ? e.message : 'Search failed'
results.value = []
} finally {
loading.value = false
}
}, debounceMs)
const filteredResults = computed(() => {
return results.value
})
const clear = () => {
query.value = ''
results.value = []
error.value = null
}
return {
query,
results: filteredResults,
loading,
error,
search: debouncedSearch,
clear
}
}
示例:usePagination
// composables/usePagination.ts
import { ref, computed } from 'vue'
interface UsePaginationOptions {
pageSize?: number
total?: number
}
export function usePagination(options: UsePaginationOptions = {}) {
const { pageSize = 10, total = 0 } = options
const currentPage = ref(1)
const totalItems = ref(total)
const totalPages = computed(() =>
Math.ceil(totalItems.value / pageSize)
)
const hasNextPage = computed(() =>
currentPage.value < totalPages.value
)
const hasPrevPage = computed(() =>
currentPage.value > 1
)
const nextPage = () => {
if (hasNextPage.value) {
currentPage.value++
}
}
const prevPage = () => {
if (hasPrevPage.value) {
currentPage.value--
}
}
const goToPage = (page: number) => {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
const offset = computed(() => (currentPage.value - 1) * pageSize)
const reset = () => {
currentPage.value = 1
}
return {
currentPage,
totalPages,
totalItems,
pageSize,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage,
offset,
reset
}
}
组合多个 Composables
真正的威力在于组合使用:
<script setup>
import { watch } from 'vue'
import { useSearch } from './composables/useSearch'
import { usePagination } from './composables/usePagination'
const {
query,
results,
loading,
error,
search
} = useSearch({
searchFn: async (q) => {
const res = await fetch(`/api/items?q=${q}&offset=${pagination.offset}&limit=${pagination.pageSize}`)
const data = await res.json()
pagination.totalItems = data.total
return data.items
}
})
const pagination = usePagination({ pageSize: 20 })
// 页码变化时重新搜索
watch(() => pagination.currentPage, () => {
if (query.value) {
search()
}
})
</script>
迁移策略
1. 新功能使用 Composition API
不要重写现有代码,新功能直接使用 <script setup>。
2. 逐步提取 Composables
找到重复逻辑,提取为 Composables。
3. 使用 defineProps 和 defineEmits
<script setup>
const props = defineProps<{
title: string
modelValue?: string
}>()
const emit = defineEmits<{
'update:modelValue': [value: string]
}>()
</script>
4. 使用 provide/inject 替代全局状态
const UserKey: InjectionKey<UserContext> = Symbol()
export function provideUser(user: UserContext) {
provide(UserKey, user)
}
export function useUser() {
const user = inject(UserKey)
if (!user) {
throw new Error('useUser must be used within provideUser')
}
return user
}
常见陷阱
1. 解构丢失响应式
// ❌ 错误:解构后失去响应式
const { count, increment } = useCounter()
count // 不是响应式的
// ✅ 正确:保持 ref 的引用
const counter = useCounter()
counter.count // 响应式
2. 忘记解包 ref
// ❌ 错误
const count = ref(0)
console.log(count) // Ref 对象
// ✅ 正确
console.log(count.value) // 0
3. 在异步函数中使用响应式数据
// ❌ 错误:闭包捕获了旧值
async function fetchData() {
const currentPage = page.value
setTimeout(() => {
console.log(currentPage) // 可能是旧值
}, 1000)
}
// ✅ 正确:始终访问最新值
async function fetchData() {
setTimeout(() => {
console.log(page.value) // 最新值
}, 1000)
}
总结
Composition API 不是银弹,但它确实解决了 Options API 在大型组件中的逻辑碎片化问题。通过提取可复用的 Composables,你可以构建更模块化、更可测试的代码。
迁移不需要一步到位。新功能使用 Composition API,逐步重构旧代码,最终你会发现整个项目的代码质量都有提升。
记住:好的代码是读得懂的代码。Composition API 让相关逻辑聚集在一起,这就是它最大的价值。
0 评论
评论区
登录 后参与评论