Vue3组合式API在大型项目里用得不对,代码会比Options API更乱:setup里几百行逻辑堆在一起、状态到处暴露、复用靠复制粘贴。这篇文章以Vue3生态为主线,讲清composable函数的拆分原则、Pinia的状态组织方式,以及两者配合时的边界划分,均来自可落地的工程实践。
composable函数拆分:按职责而非按页面
组合式函数(composable)是use开头、返回响应式状态的普通函数。常见的错误做法是为每个页面写一个大而全的useXxxPage,把请求、表单、弹窗、权限全塞进去。正确拆分维度是”可复用的职责”:
// usePagination.js —— 列表分页逻辑,任何列表页可复用
import { ref, computed } from 'vue'
export function usePagination(fetchFn, defaultSize = 20) {
const page = ref(1)
const size = ref(defaultSize)
const total = ref(0)
const loading = ref(false)
const pageCount = computed(() => Math.ceil(total.value / size.value))
async function load() {
loading.value = true
try {
const { data, total: t } = await fetchFn({ page: page.value, size: size.value })
total.value = t
return data
} finally {
loading.value = false
}
}
function next() { if (page.value < pageCount.value) page.value++ }
function prev() { if (page.value > 1) page.value-- }
return { page, size, total, loading, pageCount, load, next, prev }
}
拆分判断标准:这段逻辑换一个页面还成立吗?成立就抽成composable,不成立就留在页面内。请求封装(useRequest)、表格选择(useSelection)、弹窗开关(useModal)是复用率最高的三类。
Pinia状态组织:按业务域划分store
状态管理用Pinia,store按业务域划分(用户、订单、商品),不要按页面划分。页面级数据放组件内,跨页面共享或需要持久化的才进store。Store定义保持三个约束:state只放数据不做计算,getter承担派生值,action里处理异步与副作用。
// stores/order.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useOrderStore = defineStore('order', () => {
const orders = ref([])
const statusFilter = ref('ALL')
const filtered = computed(() =>
statusFilter.value === 'ALL'
? orders.value
: orders.value.filter(o => o.status === statusFilter.value)
)
async function fetchOrders(params) {
const { data } = await api.get('/orders', { params })
orders.value = data.list
}
async function cancelOrder(id) {
await api.post(`/orders/${id}/cancel`)
const item = orders.value.find(o => o.id === id)
if (item) item.status = 'CANCELLED'
}
return { orders, statusFilter, filtered, fetchOrders, cancelOrder }
})
组合式写法(setup store)比options写法更贴合TypeScript的类型推导,项目启用TS时优先使用。
composable与store的边界:谁放什么
两者混用最容易出乱子。边界原则一句话:store存共享状态,composable封装行为逻辑。举例说明:购物车数据本身在cartStore里;”添加商品后弹toast并刷新角标”这种带UI副作用的流程,封装成useAddToCart之类的composable,内部调用store。反过来,不要在composable里私存一份本该全局共享的状态,否则两个组件各自实例化composable后状态不同步,这类bug排查成本很高。
composable需要读全局状态时,函数内部调用useXxxStore()即可:
// composables/useCartCheckout.js
import { useCartStore } from '@/stores/cart'
import { useToast } from '@/composables/useToast'
export function useCartCheckout() {
const cart = useCartStore()
const toast = useToast()
async function checkout() {
if (!cart.items.length) {
toast.warn('购物车为空')
return
}
await cart.submitOrder()
toast.success('下单成功')
}
return { checkout }
}
TypeScript类型约束与SSR注意事项
大型项目配套TypeScript时,composable的返回值用显式类型或as const收窄,避免调用侧拿到宽泛类型。reactive对象解构后会丢失响应性,返回时用toRefs保持:
export function useUserQuery() {
const state = reactive({ list: [], loading: false, error: null })
async function search(kw: string) { /* 请求逻辑 */ }
return { ...toRefs(state), search }
}
// 调用侧解构不丢响应性
const { list, loading, search } = useUserQuery()
SSR场景(Nuxt)下两个注意点:composable里禁止在请求期间访问window、localStorage,需要时放在onMounted或客户端分支内;每个请求都要独立的store实例,Nuxt会自动处理,但自研SSR要在请求入口手动为每个请求创建pinia并传递。
复用资产的项目组织方式
src目录下的推荐结构:composables/目录按模块分子目录(composables/user/、composables/order/),每个composable一个文件加一个同名测试;通用度高的抽到独立package(monorepo里作为共享包),配合vitest单测覆盖核心逻辑。衡量复用效果的实际指标:新列表页接入分页加请求逻辑的改动行数,成熟项目里应该在30行以内,超过这个数说明composable抽象还不到位,回头补抽即可。组合式API的价值不在语法新,而在把”状态、逻辑、副作用”三者组织成可测试、可搬运的独立单元,工程规范跟得上,项目规模翻倍时代码依然可控。
原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-jin-jie-da-xing-xiang-mu-zhuang-tai-guan/