组合式函数 #
什么是组合式函数? #
组合式函数(Composables)是 Vue 3 中复用状态逻辑的一种方式。在 Pinia 的 Setup Store 中,我们可以充分利用组合式函数来组织和复用代码。
基本用法 #
简单的组合式函数 #
ts
// composables/useCounter.ts
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const double = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
function reset() {
count.value = initialValue
}
return {
count,
double,
increment,
decrement,
reset
}
}
ts
// stores/counter.ts
import { defineStore } from 'pinia'
import { useCounter } from '@/composables/useCounter'
export const useCounterStore = defineStore('counter', () => {
const { count, double, increment, decrement, reset } = useCounter(0)
return { count, double, increment, decrement, reset }
})
常用组合式函数 #
异步请求处理 #
ts
// composables/useAsync.ts
import { ref, Ref } from 'vue'
interface AsyncState<T> {
data: Ref<T | null>
loading: Ref<boolean>
error: Ref<Error | null>
execute: (...args: any[]) => Promise<T | null>
}
export function useAsync<T>(
asyncFn: (...args: any[]) => Promise<T>,
immediate = false
): AsyncState<T> {
const data = ref<T | null>(null) as Ref<T | null>
const loading = ref(false)
const error = ref<Error | null>(null)
async function execute(...args: any[]): Promise<T | null> {
loading.value = true
error.value = null
try {
const result = await asyncFn(...args)
data.value = result
return result
} catch (e) {
error.value = e instanceof Error ? e : new Error(String(e))
return null
} finally {
loading.value = false
}
}
if (immediate) {
execute()
}
return { data, loading, error, execute }
}
ts
// stores/product.ts
import { defineStore } from 'pinia'
import { computed } from 'vue'
import { useAsync } from '@/composables/useAsync'
interface Product {
id: number
name: string
price: number
}
export const useProductStore = defineStore('product', () => {
const products = ref<Product[]>([])
// 使用 useAsync 处理异步请求
const { loading, error, execute: fetchProducts } = useAsync(
async () => {
const response = await fetch('/api/products')
products.value = await response.json()
return products.value
}
)
const productCount = computed(() => products.value.length)
return {
products,
loading,
error,
productCount,
fetchProducts
}
})
分页处理 #
ts
// composables/usePagination.ts
import { ref, computed, watch } from 'vue'
interface PaginationOptions {
pageSize?: number
total?: number
}
export function usePagination(options: PaginationOptions = {}) {
const { pageSize = 10, total = 0 } = options
const currentPage = ref(1)
const itemsPerPage = ref(pageSize)
const totalItems = ref(total)
const totalPages = computed(() =>
Math.ceil(totalItems.value / itemsPerPage.value)
)
const offset = computed(() =>
(currentPage.value - 1) * itemsPerPage.value
)
const hasNextPage = computed(() => currentPage.value < totalPages.value)
const hasPrevPage = computed(() => currentPage.value > 1)
function nextPage() {
if (hasNextPage.value) {
currentPage.value++
}
}
function prevPage() {
if (hasPrevPage.value) {
currentPage.value--
}
}
function goToPage(page: number) {
if (page >= 1 && page <= totalPages.value) {
currentPage.value = page
}
}
function setTotal(total: number) {
totalItems.value = total
}
return {
currentPage,
itemsPerPage,
totalItems,
totalPages,
offset,
hasNextPage,
hasPrevPage,
nextPage,
prevPage,
goToPage,
setTotal
}
}
ts
// stores/article.ts
import { defineStore } from 'pinia'
import { ref, watch } from 'vue'
import { usePagination } from '@/composables/usePagination'
import { useAsync } from '@/composables/useAsync'
interface Article {
id: number
title: string
content: string
}
export const useArticleStore = defineStore('article', () => {
const articles = ref<Article[]>([])
const pagination = usePagination({ pageSize: 10 })
const { loading, execute: fetchArticles } = useAsync(async () => {
const response = await fetch(
`/api/articles?page=${pagination.currentPage.value}&size=${pagination.itemsPerPage.value}`
)
const data = await response.json()
articles.value = data.items
pagination.setTotal(data.total)
return data
})
// 页码变化时重新获取数据
watch(pagination.currentPage, () => {
fetchArticles()
})
return {
articles,
loading,
...pagination,
fetchArticles
}
})
表单处理 #
ts
// composables/useForm.ts
import { ref, reactive, computed } from 'vue'
interface FormOptions<T> {
initialValues: T
validate?: (values: T) => Record<string, string>
onSubmit: (values: T) => Promise<void> | void
}
export function useForm<T extends Record<string, any>>(options: FormOptions<T>) {
const { initialValues, validate, onSubmit } = options
const values = reactive({ ...initialValues }) as T
const errors = reactive<Record<string, string>>({})
const touched = reactive<Record<string, boolean>>({})
const isSubmitting = ref(false)
const isValid = computed(() => Object.keys(errors).length === 0)
const isDirty = computed(() =>
JSON.stringify(values) !== JSON.stringify(initialValues)
)
function setFieldValue(field: keyof T, value: any) {
(values as any)[field] = value
touched[field as string] = true
validateField(field)
}
function validateField(field: keyof T) {
if (validate) {
const validationErrors = validate(values)
if (validationErrors[field as string]) {
errors[field as string] = validationErrors[field as string]
} else {
delete errors[field as string]
}
}
}
function validateAll() {
if (validate) {
const validationErrors = validate(values)
Object.keys(errors).forEach(key => delete errors[key])
Object.assign(errors, validationErrors)
}
}
async function handleSubmit() {
validateAll()
if (!isValid.value) return
isSubmitting.value = true
try {
await onSubmit(values)
} finally {
isSubmitting.value = false
}
}
function reset() {
Object.assign(values, initialValues)
Object.keys(errors).forEach(key => delete errors[key])
Object.keys(touched).forEach(key => delete touched[key])
}
return {
values,
errors,
touched,
isSubmitting,
isValid,
isDirty,
setFieldValue,
validateField,
validateAll,
handleSubmit,
reset
}
}
ts
// stores/auth.ts
import { defineStore } from 'pinia'
import { useForm } from '@/composables/useForm'
interface LoginForm {
email: string
password: string
}
export const useAuthStore = defineStore('auth', () => {
const user = ref(null)
const token = ref<string | null>(null)
const form = useForm<LoginForm>({
initialValues: {
email: '',
password: ''
},
validate: (values) => {
const errors: Record<string, string> = {}
if (!values.email) {
errors.email = 'Email is required'
} else if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(values.email)) {
errors.email = 'Invalid email format'
}
if (!values.password) {
errors.password = 'Password is required'
} else if (values.password.length < 6) {
errors.password = 'Password must be at least 6 characters'
}
return errors
},
onSubmit: async (values) => {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(values)
})
const data = await response.json()
user.value = data.user
token.value = data.token
}
})
function logout() {
user.value = null
token.value = null
form.reset()
}
return {
user,
token,
form,
logout
}
})
选择/多选处理 #
ts
// composables/useSelection.ts
import { ref, computed } from 'vue'
export function useSelection<T>(key: keyof T = 'id' as keyof T) {
const selectedItems = ref<T[]>([])
const lastSelected = ref<T | null>(null)
const isSelected = computed(() => {
return (item: T) => {
return selectedItems.value.some(
selected => selected[key] === item[key]
)
}
})
const selectedCount = computed(() => selectedItems.value.length)
const hasSelection = computed(() => selectedCount.value > 0)
function select(item: T) {
if (!isSelected.value(item)) {
selectedItems.value.push(item)
lastSelected.value = item
}
}
function deselect(item: T) {
const index = selectedItems.value.findIndex(
selected => selected[key] === item[key]
)
if (index !== -1) {
selectedItems.value.splice(index, 1)
}
}
function toggle(item: T) {
if (isSelected.value(item)) {
deselect(item)
} else {
select(item)
}
}
function selectAll(items: T[]) {
selectedItems.value = [...items]
}
function deselectAll() {
selectedItems.value = []
}
function selectRange(items: T[], startItem: T, endItem: T) {
const startIndex = items.findIndex(
item => item[key] === startItem[key]
)
const endIndex = items.findIndex(
item => item[key] === endItem[key]
)
const [from, to] = startIndex < endIndex
? [startIndex, endIndex]
: [endIndex, startIndex]
for (let i = from; i <= to; i++) {
if (!isSelected.value(items[i])) {
selectedItems.value.push(items[i])
}
}
}
return {
selectedItems,
lastSelected,
isSelected,
selectedCount,
hasSelection,
select,
deselect,
toggle,
selectAll,
deselectAll,
selectRange
}
}
ts
// stores/file.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'
import { useSelection } from '@/composables/useSelection'
interface File {
id: string
name: string
size: number
type: string
}
export const useFileStore = defineStore('file', () => {
const files = ref<File[]>([])
const loading = ref(false)
const selection = useSelection<File>('id')
async function fetchFiles() {
loading.value = true
try {
const response = await fetch('/api/files')
files.value = await response.json()
} finally {
loading.value = false
}
}
async function deleteSelected() {
const ids = selection.selectedItems.value.map(f => f.id)
await fetch('/api/files', {
method: 'DELETE',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ ids })
})
files.value = files.value.filter(f => !ids.includes(f.id))
selection.deselectAll()
}
return {
files,
loading,
...selection,
fetchFiles,
deleteSelected
}
})
最佳实践 #
1. 单一职责 #
每个组合式函数只负责一个功能:
ts
// 好的做法
useAsync() // 处理异步请求
usePagination() // 处理分页
useSelection() // 处理选择
// 不好的做法
useEverything() // 处理所有事情
2. 命名规范 #
使用 use 前缀命名组合式函数:
ts
// 推荐
export function useCounter() { /* ... */ }
export function useAsync() { /* ... */ }
export function usePagination() { /* ... */ }
// 不推荐
export function counter() { /* ... */ }
export function createAsync() { /* ... */ }
3. 返回响应式引用 #
返回 ref 和 computed,而不是原始值:
ts
// 推荐
export function useCounter() {
const count = ref(0)
const double = computed(() => count.value * 2)
return { count, double }
}
// 不推荐
export function useCounter() {
let count = 0
return { count, double: count * 2 } // 失去响应性
}
4. 接受 ref 或 reactive 作为参数 #
ts
export function useSearch(items: Ref<any[]>, searchKey: string) {
const query = ref('')
const results = computed(() => {
if (!query.value) return items.value
return items.value.filter(item =>
item[searchKey].toLowerCase().includes(query.value.toLowerCase())
)
})
return { query, results }
}
下一步 #
现在你已经掌握了组合式函数的使用,接下来让我们学习 Store 的组合。
- Store组合 - 多个 Store 协作
最后更新:2026-03-28