Vue3组合式API实战:从Options API迁移到Composables的完整重构方案

Vue3组合式API解决了Options API的什么问题

Options API按选项组织代码(data、methods、computed、watch分散在不同选项里),一个功能的逻辑被拆成好几块,组件超过300行就难以维护。组合式API用Composables把同一功能的逻辑聚合到一起,多个组件间复用只需调用一个函数。这篇文章用实际项目案例演示从Options API到Composables的重构过程,以及TypeScript实战中的类型约束写法。

Options API到Composition API的基础迁移

迁移前的Options API写法:

export default {
  data() {
    return {
      userList: [],
      loading: false,
      searchKey: '',
      currentPage: 1,
      pageSize: 10,
      total: 0,
    }
  },
  computed: {
    filteredUsers() {
      return this.userList.filter(u => u.name.includes(this.searchKey))
    }
  },
  methods: {
    async fetchUsers() {
      this.loading = true
      try {
        const res = await api.getUsers({ page: this.currentPage, size: this.pageSize })
        this.userList = res.data.list
        this.total = res.data.total
      } finally {
        this.loading = false
      }
    },
    handleSearch() {
      this.currentPage = 1
      this.fetchUsers()
    }
  },
  mounted() {
    this.fetchUsers()
  }
}

迁移后的Composition API写法:

<script setup lang="ts">
import { ref, computed, onMounted } from 'vue'

const userList = ref<User[]>([])
const loading = ref(false)
const searchKey = ref('')
const currentPage = ref(1)
const pageSize = ref(10)
const total = ref(0)

const filteredUsers = computed(() =>
  userList.value.filter(u => u.name.includes(searchKey.value))
)

async function fetchUsers() {
  loading.value = true
  try {
    const res = await api.getUsers({ page: currentPage.value, size: pageSize.value })
    userList.value = res.data.list
    total.value = res.data.total
  } finally {
    loading.value = false
  }
}

function handleSearch() {
  currentPage.value = 1
  fetchUsers()
}

onMounted(() => fetchUsers())
</script>

功能一致,但所有相关变量和函数在同一个作用域内,阅读时不需要在data/computed/methods之间跳转。

提取Composables实现逻辑复用

当多个组件都需要用户列表的分页加载能力时,把它抽成Composable:

// composables/useUserList.ts
import { ref, computed } from 'vue'
import type { User } from '@/types'

export function useUserList() {
  const userList = ref<User[]>([])
  const loading = ref(false)
  const currentPage = ref(1)
  const pageSize = ref(10)
  const total = ref(0)

  async function fetchUsers(params?: Partial<{ keyword: string }>) {
    loading.value = true
    try {
      const res = await api.getUsers({
        page: currentPage.value,
        size: pageSize.value,
        ...params,
      })
      userList.value = res.data.list
      total.value = res.data.total
    } finally {
      loading.value = false
    }
  }

  function changePage(page: number) {
    currentPage.value = page
    fetchUsers()
  }

  const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

  return {
    userList,
    loading,
    currentPage,
    pageSize,
    total,
    totalPages,
    fetchUsers,
    changePage,
  }
}

组件中使用:

<script setup lang="ts">
import { useUserList } from '@/composables/useUserList'
import { onMounted } from 'vue'

const { userList, loading, totalPages, currentPage, changePage, fetchUsers } = useUserList()

onMounted(() => fetchUsers())
</script>

通用分页Composables抽离

分页逻辑不仅用于用户列表,还用于订单、商品等场景。进一步抽象:

// composables/usePagination.ts
import { ref, computed, type Ref } from 'vue'

interface PaginationOptions<T> {
  fetchFn: (params: { page: number; size: number; [k: string]: any }) => Promise<{ list: T[]; total: number }>
  pageSize?: number
}

export function usePagination<T>(options: PaginationOptions<T>) {
  const list = ref<T[]>([]) as Ref<T[]>
  const loading = ref(false)
  const currentPage = ref(1)
  const pageSize = ref(options.pageSize || 10)
  const total = ref(0)

  async function fetch(extraParams?: Record<string, any>) {
    loading.value = true
    try {
      const res = await options.fetchFn({
        page: currentPage.value,
        size: pageSize.value,
        ...extraParams,
      })
      list.value = res.list
      total.value = res.total
    } finally {
      loading.value = false
    }
  }

  const totalPages = computed(() => Math.ceil(total.value / pageSize.value))

  function goToPage(page: number) {
    currentPage.value = page
    fetch()
  }

  return { list, loading, currentPage, pageSize, total, totalPages, fetch, goToPage }
}

任何需要分页的组件,三行代码接入:

const { list, loading, totalPages, currentPage, goToPage, fetch } = usePagination({
  fetchFn: (params) => api.getOrders(params).then(r => ({ list: r.data.list, total: r.data.total })),
  pageSize: 20,
})

TypeScript类型约束与前端工程化

Composables配合TypeScript的关键是泛型约束。上面的usePagination通过泛型T约束列表数据类型,组件调用时自动推导出list的类型。

类型定义文件组织:

// types/index.ts
export interface User {
  id: number
  name: string
  email: string
  role: 'admin' | 'editor' | 'viewer'
  createdAt: string
}

export interface PaginatedResponse<T> {
  list: T[]
  total: number
}

工程化层面,Composables目录结构:

src/
  composables/
    usePagination.ts
    useUserList.ts
    useAuth.ts
    useTheme.ts
  types/
    index.ts
  views/
    UserList.vue
    OrderList.vue

ESLint规则建议开启:vue/component-api-style强制使用<script setup>vue/no-ref-as-operand防止ref误用。

响应式布局下的Composables设计

移动端和PC端可能需要不同的分页策略,Composables可以封装响应式逻辑:

import { computed } from 'vue'
import { useBreakpoints } from '@/composables/useBreakpoints'

export function useResponsivePagination() {
  const { isMobile } = useBreakpoints()
  const mobilePageSize = 5
  const desktopPageSize = 20

  const pageSize = computed(() => isMobile.value ? mobilePageSize : desktopPageSize)

  return { pageSize }
}

从Options API迁移到Composables不是简单的语法转换,而是代码组织方式的升级。核心原则:一个Composables只做一件事,函数签名清晰,返回值显式声明类型。这套方案在团队协作项目中显著降低代码理解成本,新人接手一个模块只需要看对应的Composables文件,不用翻遍整个组件。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-shi-zhan-cong-optionsapi-qian-yi-dao/

(0)
小编小编
上一篇 13小时前
下一篇 13小时前

相关推荐