← 博客
技术2026-09-023 次阅读

Vue 3 组合式 API 实战:在 MES 系统中如何优雅管理复杂组件状态

# Vue3,前端开发,组合式 API# Composition API# MES# 状态管理
Vue3

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 行,逻辑清晰度肉眼可见——这就是工程的价值。

评论 (0)