Vue 3 组合式 API 实战:在 MES 系统中如何优雅管理复杂组件状态
Vue 3 组合式 API 实战:在 MES 系统中如何优雅管理复杂组件状态
在 MES 系统的工站管理页,我曾见过这样的代码:
data() {
return {
stationList: [],
loading: false,
searchKeyword: '',
page: 1,
pageSize: 20,
total: 0,
filterStatus: '',
sortField: '',
sortOrder: '',
// 还有 30+ 个响应式变量...
}
}
这还不是最离谱的。最离谱的是这些变量散落在 data、methods、computed 里,想找一个功能相关的逻辑,得翻半屏代码。MES 系统不是简单的 CRUD,工站管理页光是状态就超过 50 个,用 Options API 写,维护成本会爆炸。
解决方案:组合式 API + Composition Functions
Vue 3 的 Composition API 不是用来炫技的,它是为了解决大型组件的状态管理混乱而生的。我把工站管理页拆成三个组合函数:useStationList(数据获取)、useStationFilter(筛选逻辑)、useStationForm(表单管理),组件里只剩布局逻辑。
第一步:从 Options API 迁移
老项目的 Options API 代码不能一夜之间全改,我的做法是按功能模块拆分,新页直接用 Composition API,老页逐步重构。
迁移前的工站列表页:
export default {
data() {
return {
stationList: [],
loading: false,
searchKeyword: '',
page: 1,
pageSize: 20,
total: 0,
// ... 50+ 个变量
}
},
methods: {
fetchList() {
this.loading = true
api.getStations({ page: this.page, keyword: this.searchKeyword })
.then(res => {
this.stationList = res.data
this.total = res.total
})
.finally(() => { this.loading = false })
},
handleSearch() {
this.page = 1
this.fetchList()
},
// ... 还有 30+ 个方法
},
watch: {
searchKeyword(newVal) {
// 防抖搜索逻辑...
}
}
}
迁移后的版本:
import { ref, reactive, onMounted } from 'vue'
import { useStationList } from './composables/useStationList'
import { useDebounceFn } from '@vueuse/core'
export default {
setup() {
// 把逻辑封装成组合函数
const { stationList, loading, fetchList } = useStationList()
const searchKeyword = ref('')
const handleSearch = useDebounceFn(() => {
fetchList()
}, 300)
onMounted(() => fetchList())
return {
stationList,
loading,
searchKeyword,
handleSearch
}
}
}
组合函数内部把所有相关的状态、方法、副作用封装在一起,组件层只负责调用和渲染——这就是单一职责原则在 Vue 里的体现。
第二步:实现 useStationList 组合函数
这是核心组合函数,负责所有与列表数据相关的逻辑:
import { ref, computed } from 'vue'
import api from '@/api/station'
export function useStationList() {
const stationList = ref([])
const loading = ref(false)
const total = ref(0)
const page = ref(1)
const pageSize = ref(20)
const searchKeyword = ref('')
const filterStatus = ref('')
// 查询参数计算属性
const queryParams = computed(() => ({
page: page.value,
pageSize: pageSize.value,
keyword: searchKeyword.value,
status: filterStatus.value
}))
async function fetchList() {
loading.value = true
try {
const res = await api.getStations(queryParams.value)
stationList.value = res.data
total.value = res.total
} catch (err) {
console.error('获取工站列表失败', err)
} finally {
loading.value = false
}
}
function handlePageChange(newPage) {
page.value = newPage
fetchList()
}
function handleSearch() {
page.value = 1
searchKeyword.value = searchKeyword.value.trim()
fetchList()
}
// 返回所有对外暴露的 API
return {
stationList,
loading,
total,
queryParams,
fetchList,
handlePageChange,
handleSearch
}
}
关键点:
ref和computed都是响应式数据,模板里绑定后会自动更新;queryParams是计算属性,依赖项变化时自动重新计算,避免手动拼装对象;fetchList方法里用loading.value控制加载状态,避免重复点击;- 只返回必要的 API,组合函数内部的私有变量(如
page)不应该被外部直接修改。
第三步:处理复杂交互
MES 系统的工站管理页有个复杂功能:批量启用/禁用工站。勾选列表里的多个工站,点击"批量启用",后端批量更新。
用 Options API 写是这样:
data() {
return {
selectedIds: [],
// ... 其他数据
}
},
methods: {
handleSelect(id, checked) {
if (checked) {
this.selectedIds.push(id)
} else {
this.selectedIds = this.selectedIds.filter(i => i !== id)
}
},
async handleBatchEnable() {
await api.batchEnable({ ids: this.selectedIds })
this.$message.success('启用成功')
this.fetchList()
}
}
组合函数版:
export function useStationSelection() {
const selectedIds = ref([])
function toggleSelect(id) {
const index = selectedIds.value.indexOf(id)
if (index > -1) {
selectedIds.value.splice(index, 1)
} else {
selectedIds.value.push(id)
}
}
function clearSelection() {
selectedIds.value = []
}
return {
selectedIds,
toggleSelect,
clearSelection
}
}
在组件里合并使用:
setup() {
const { stationList, loading, fetchList } = useStationList()
const { selectedIds, toggleSelect, clearSelection } = useStationSelection()
async function handleBatchEnable() {
if (selectedIds.value.length === 0) {
ElMessage.warning('请先选择工站')
return
}
await api.batchEnable({ ids: selectedIds.value })
ElMessage.success('启用成功')
clearSelection()
fetchList()
}
return {
stationList,
loading,
selectedIds,
toggleSelect,
handleBatchEnable,
clearSelection
}
}
组合函数的好处是逻辑复用——如果有其他列表页也需要批量操作,直接复用 useStationSelection 即可。
第四步:状态持久化
MES 系统里有个场景:用户刷新页面后,筛选条件和分页状态丢失,需要重新配置。这个需求用 watchEffect + localStorage 实现:
import { watchEffect } from 'vue'
import { useStorage } from '@vueuse/core'
export function usePersistedState(key, initialValue) {
return useStorage(key, initialValue)
}
在组合函数里调用:
const searchKeyword = usePersistedState('station:search', '')
const page = usePersistedState('station:page', 1)
const filterStatus = usePersistedState('station:status', '')
这样用户刷新页面后,筛选条件会自动恢复,体验提升明显。
踩坑记录:五个真实案例
- 坑 1:ref 和 reactive 混用导致响应式丢失。有些开发者在
reactive对象里嵌套ref,访问时要写成obj.refProp.value,容易忘记。最佳实践是:简单对象用 reactive,复杂状态用 ref,组件里保持一致。 - 坑 2:组合函数里直接操作 DOM。有人在
useStationList里写了document.getElementById,这违反了 Vue 的响应式原则。组合函数应该只处理状态和逻辑,DOM 操作交给模板或nextTick。 - 坑 3:async/await 在模板里直接写。模板里不能直接写
v-for="item in await fetchList()",这是非法语法。正确做法是在setup里异步获取数据,模板只负责渲染。 - 坑 4:组合函数闭包问题。在循环里创建组合函数会导致状态混乱,比如:
// 错误写法
stations.value.forEach(station => {
const { isSelected } = useStationSelection() // 每个 station 都是独立的响应式状态?
})
组合函数应该在 setup 顶层调用,不要嵌套在循环或回调里。
- 坑 5:useDebounceFn 参数传递错误。防抖函数里访问外部变量要注意闭包,正确的写法是:
const handleSearch = useDebounceFn(() => {
console.log(searchKeyword.value) // 正确:访问的是最新值
}, 300)
进阶:用 Pinia 管理跨组件状态
工站列表页和工站详情页需要共享某些状态(如当前选中的工站),用组合函数传递 props 太麻烦。Pinia 是 Vue 3 官方推荐的状态管理方案:
// stores/selectedStation.js
import { defineStore } from 'pinia'
export const useSelectedStationStore = defineStore('selectedStation', {
state: () => ({
current: null
}),
actions: {
setCurrent(station) {
this.current = station
},
clear() {
this.current = null
}
}
})
在组件里调用:
import { useSelectedStationStore } from '@/stores/selectedStation'
const store = useSelectedStationStore()
// 在任何组件里都能访问 store.current
总结
Vue 3 组合式 API 在 MES 系统中的应用,核心是把复杂逻辑拆成独立的组合函数,每个函数负责单一职责,组件层只负责组合和渲染。
- 组合函数:把所有相关的状态、方法、副作用封装在一起,逻辑复用、易于测试;
- Composition Functions:遵循单一职责原则,一个函数管一个业务域(列表、筛选、表单);
- 响应式工具:
ref、reactive、computed、watchEffect根据场景选择; - 状态管理:同组件用组合函数,跨组件用 Pinia。
附:组合式 API 最佳实践清单
- 组合函数在 setup 顶层调用,不要嵌套在循环或回调里;
- 组合函数内部不直接操作 DOM;
- 返回的 API 尽量只读,修改操作通过方法暴露;
- 用计算属性自动派生数据,避免手动维护冗余状态;
- 复杂场景用 Pinia,简单场景用组合函数。
Vue 3 组合式 API 不是银弹,但它确实让大型组件的可维护性提升了一个台阶。MES 系统里,一个工站管理页的代码量从 400 行降到 150 行,逻辑清晰度肉眼可见——这就是工程的价值。