Vue3的组合式API(Composition API)改变了组件逻辑的组织方式,而Composable组合式函数则是这一体系下实现状态复用和逻辑抽取的核心模式。通过将响应式状态、计算属性、副作用等封装为独立函数,多个组件之间可以共享复杂逻辑而不受Options API中mixin命名冲突和数据来源不透明的困扰。本文从Composable设计原则出发,结合实际项目中的表单验证、数据请求、分页等场景,给出Vue3组合式函数的完整设计实践。
Composable组合式函数设计原则与响应式基础
Composable是利用Vue3组合式API封装的可复用逻辑函数,命名约定以 use 前缀开头,如 useMousePosition、 useFetch。核心设计原则有三条:单一职责、显式输入输出、响应式驱动。
单一职责要求每个Composable只关注一个领域逻辑。 useFormValidation 只处理表单校验,不混入数据请求逻辑; usePagination 只管理分页状态,不关心数据来源。
显式输入输出要求Composable通过参数接收配置,通过返回值暴露状态和方法。调用方能清楚知道数据从哪里来、有哪些可用方法,避免mixin中隐式注入的属性导致的来源不透明问题。
响应式驱动要求内部状态使用 ref、 reactive、 computed 等响应式API,确保状态变化自动触发依赖更新。
一个基础示例:
import { ref, onMounted, onUnmounted } from 'vue'
export function useMousePosition() {
const x = ref(0)
const y = ref(0)
function update(event) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
在组件中使用:
<script setup>
import { useMousePosition } from './composables/useMousePosition'
const { x, y } = useMousePosition()
</script>
<template>
<p>Mouse: {{ x }}, {{ y }}</p>
</template>
生命周期钩子( onMounted、 onUnmounted)必须在 setup 同步执行期间调用,因此Composable函数必须在 <script setup> 顶层调用,不能放在异步回调中。
表单验证Composable与异步校验实现
表单验证是前端项目中的高频需求。通过Composable将验证逻辑从组件中抽离,实现多表单复用:
import { ref, reactive, computed } from 'vue'
export function useFormValidation(initialValues, rules) {
const values = reactive({ ...initialValues })
const errors = reactive({})
const touched = reactive({})
const isSubmitting = ref(false)
// 单字段验证
function validateField(field) {
const fieldRules = rules[field]
if (!fieldRules) return true
for (const rule of fieldRules) {
const result = rule(values[field], values)
if (result !== true) {
errors[field] = result
return false
}
}
errors[field] = ''
return true
}
// 全表单验证
function validateAll() {
let valid = true
for (const field in rules) {
if (!validateField(field)) {
valid = false
}
}
return valid
}
// 异步验证(如远程检查用户名是否已注册)
async function validateFieldAsync(field, asyncFn) {
if (!validateField(field)) return false
const result = await asyncFn(values[field])
if (!result.valid) {
errors[field] = result.message
return false
}
return true
}
// 处理字段blur事件
function handleBlur(field) {
touched[field] = true
validateField(field)
}
// 处理字段input事件
function handleInput(field) {
if (touched[field]) {
validateField(field)
}
}
const isValid = computed(() => {
for (const field in rules) {
if (errors[field]) return false
}
return true
})
return {
values, errors, touched, isSubmitting, isValid,
validateField, validateAll, validateFieldAsync,
handleBlur, handleInput
}
}
定义验证规则和组件中使用:
// rules.js
export const loginRules = {
username: [
(v) => !!v || '请输入用户名',
(v) => v.length >= 3 || '用户名至少3个字符',
],
password: [
(v) => !!v || '请输入密码',
(v) => v.length >= 8 || '密码至少8位',
(v) => /[A-Z]/.test(v) || '密码需包含大写字母',
],
}
// LoginForm.vue
<script setup>
import { useFormValidation } from './composables/useFormValidation'
import { loginRules } from './rules'
const { values, errors, validateAll, handleBlur, handleInput } =
useFormValidation(
{ username: '', password: '' },
loginRules
)
async function handleSubmit() {
if (!validateAll()) return
// 提交逻辑
const res = await api.login(values)
// ...
}
</script>
数据请求Composable与请求缓存设计
数据请求是另一个高频场景。封装带缓存、防抖、错误处理的请求Composable:
import { ref, watch, isRef } from 'vue'
export function useFetch(url, options = {}) {
const {
immediate = true,
initialData = null,
refetch = false,
transform,
onError,
} = options
const data = ref(initialData)
const error = ref(null)
const loading = ref(false)
// 简单内存缓存
const cache = new Map()
async function execute() {
const urlValue = isRef(url) ? url.value : url
// 检查缓存
if (!refetch && cache.has(urlValue)) {
data.value = cache.get(urlValue)
return
}
loading.value = true
error.value = null
try {
const response = await fetch(urlValue)
if (!response.ok) throw new Error(`HTTP ${response.status}`)
let result = await response.json()
if (transform) result = transform(result)
data.value = result
cache.set(urlValue, result)
} catch (err) {
error.value = err
if (onError) onError(err)
} finally {
loading.value = false
}
}
if (isRef(url)) {
watch(url, execute)
}
if (immediate) {
execute()
}
return { data, error, loading, execute, refresh: execute }
}
组件中使用,支持响应式URL和自动重新请求:
<script setup>
import { ref } from 'vue'
import { useFetch } from './composables/useFetch'
const userId = ref(1)
const { data: user, error, loading } = useFetch(
() => `/api/users/${userId.value}`,
{
transform: (res) => ({
...res,
fullName: `${res.firstName} ${res.lastName}`,
}),
onError: (err) => console.error('请求失败:', err),
}
)
function switchUser(id) {
userId.value = id // 自动触发重新请求
}
</script>
分页Composable与排序状态管理
分页和排序是列表页面的通用逻辑,通过Composable统一管理状态:
import { ref, computed, reactive } from 'vue'
export function usePagination(fetchFn, options = {}) {
const {
initialPage = 1,
initialPageSize = 20,
initialSortBy = '',
initialSortOrder = 'asc',
} = options
const currentPage = ref(initialPage)
const pageSize = ref(initialPageSize)
const sortBy = ref(initialSortBy)
const sortOrder = ref(initialSortOrder)
const total = ref(0)
const items = ref([])
const loading = ref(false)
const totalPages = computed(() =>
Math.ceil(total.value / pageSize.value)
)
const paginationParams = computed(() => ({
page: currentPage.value,
pageSize: pageSize.value,
sortBy: sortBy.value,
sortOrder: sortOrder.value,
}))
async function loadData() {
loading.value = true
try {
const result = await fetchFn(paginationParams.value)
items.value = result.items
total.value = result.total
} finally {
loading.value = false
}
}
function goToPage(page) {
currentPage.value = page
loadData()
}
function changePageSize(size) {
pageSize.value = size
currentPage.value = 1
loadData()
}
function toggleSort(field) {
if (sortBy.value === field) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = field
sortOrder.value = 'asc'
}
currentPage.value = 1
loadData()
}
return {
currentPage, pageSize, sortBy, sortOrder,
total, totalPages, items, loading,
paginationParams,
goToPage, changePageSize, toggleSort, loadData,
}
}
在列表组件中组合使用:
<script setup>
import { usePagination } from './composables/usePagination'
import { useFetch } from './composables/useFetch'
const { items, currentPage, pageSize, total, totalPages,
loading, goToPage, changePageSize, toggleSort } =
usePagination(
async (params) => {
const res = await fetch(
`/api/users?page=${params.page}&size=${params.pageSize}` +
`&sort=${params.sortBy}&order=${params.sortOrder}`
)
return res.json()
},
{ initialPageSize: 15 }
)
</script>
Composable组合与依赖管理
多个Composable可以组合使用,形成更高层次的逻辑抽象。例如将数据请求和分页组合:
export function useUserList() {
const { paginationParams, ...pagination } = usePagination()
const { data, loading, error, execute } = useFetch(
() => `/api/users?${new URLSearchParams(paginationParams.value)}`,
{ immediate: false }
)
// 将分页操作和请求绑定
pagination.loadData = async () => {
await execute()
pagination.items.value = data.value.items
pagination.total.value = data.value.total
}
pagination.loadData()
return { ...pagination, error }
}
组合时注意避免循环依赖——Composable A调用Composable B,B又回调A的情况。通过将共享状态提升到父级Composable统一管理,子级Composable通过参数接收状态而非自行创建,可以有效解耦依赖关系。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3composable-zu-he-shi-han-shu-she-ji-yu-zhuang-tai-fu/