Vue3组合式API项目架构实战:从脚手架到生产部署的最佳实践

Vue3项目架构选型与初始化

Vue3组合式API配合TypeScript和Vite,是目前中大型前端项目的标准技术栈。用create-vue初始化项目,按需引入Router、Pinia、ESLint:

npm create vue@latest my-app
# 选择 TypeScript + Router + Pinia + ESLint
cd my-app && npm install

项目目录结构规划:

src/
  api/            # 接口请求封装
  assets/         # 静态资源
  components/     # 公共组件
  composables/    # 组合式函数(hooks)
  layouts/        # 布局组件
  pages/          # 页面级组件
  router/         # 路由配置
  stores/         # Pinia状态管理
  types/          # TypeScript类型定义
  utils/          # 工具函数

组合式API封装业务逻辑:Composable函数设计

把可复用的业务逻辑抽到composables目录,用ref/reactive管理响应式状态:

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

export function usePagination(fetchFn, pageSize = 10) {
  const currentPage = ref(1)
  const total = ref(0)
  const loading = ref(false)
  const data = ref([])

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

  async function fetchPage(page: number) {
    loading.value = true
    try {
      const res = await fetchFn({ page, pageSize })
      data.value = res.list
      total.value = res.total
      currentPage.value = page
    } finally {
      loading.value = false
    }
  }

  function nextPage() {
    if (currentPage.value < totalPages.value) fetchPage(currentPage.value + 1)
  }

  function prevPage() {
    if (currentPage.value > 1) fetchPage(currentPage.value - 1)
  }

  return { data, currentPage, totalPages, total, loading, fetchPage, nextPage, prevPage }
}

使用时直接解构调用,逻辑清晰且可跨组件复用:

// pages/UserList.vue
import { usePagination } from '@/composables/usePagination'
import { getUserList } from '@/api/user'

const { data, currentPage, totalPages, loading, fetchPage } = usePagination(getUserList)
onMounted(() => fetchPage(1))

Pinia状态管理:模块化Store设计

Pinia相比Vuex更轻量,用defineStore定义模块:

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useUserStore = defineStore('user', () => {
  const token = ref(localStorage.getItem('token') || '')
  const userInfo = ref(null)
  const isLoggedIn = computed(() => !!token.value)

  async function login(credentials) {
    const res = await loginApi(credentials)
    token.value = res.token
    userInfo.value = res.userInfo
    localStorage.setItem('token', res.token)
  }

  function logout() {
    token.value = ''
    userInfo.value = null
    localStorage.removeItem('token')
  }

  return { token, userInfo, isLoggedIn, login, logout }
})

接口请求层封装:统一错误处理与Token刷新

// utils/request.ts
import axios from 'axios'
import { useUserStore } from '@/stores/user'
import router from '@/router'

const request = axios.create({
  baseURL: import.meta.env.VITE_API_BASE,
  timeout: 15000
})

request.interceptors.request.use(config => {
  const userStore = useUserStore()
  if (userStore.token) {
    config.headers.Authorization = `Bearer ${userStore.token}`
  }
  return config
})

request.interceptors.response.use(
  response => response.data,
  error => {
    if (error.response?.status === 401) {
      useUserStore().logout()
      router.push('/login')
    }
    return Promise.reject(error)
  }
)

export default request

路由守卫与权限控制

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/stores/user'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/login', component: () => import('@/pages/Login.vue') },
    { path: '/dashboard', component: () => import('@/pages/Dashboard.vue'), meta: { auth: true } },
    { path: '/admin', component: () => import('@/pages/Admin.vue'), meta: { auth: true, role: 'admin' } }
  ]
})

router.beforeEach((to) => {
  const userStore = useUserStore()
  if (to.meta.auth && !userStore.isLoggedIn) return '/login'
  if (to.meta.role && userStore.userInfo?.role !== to.meta.role) return '/403'
})

生产构建优化配置

vite.config.ts关键优化项:

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus'],
          'utils': ['axios', 'dayjs', 'lodash-es']
        }
      }
    },
    chunkSizeWarningLimit: 1000
  }
})

manualChunks把第三方库拆成独立chunk,利用浏览器缓存减少重复下载。Element Plus这样的大UI库单独分包,只在需要时加载。

常见架构问题

Q: Composable函数中的异步操作如何处理竞态?
用AbortController取消前一次请求:

let controller = null

async function search(keyword) {
  controller?.abort()
  controller = new AbortController()
  try {
    const res = await fetchApi(keyword, { signal: controller.signal })
    data.value = res
  } catch (e) {
    if (e.name !== 'AbortError') throw e
  }
}

Q: 大型项目如何管理路由配置?
按功能模块拆分路由文件,用动态import实现懒加载。路由meta字段配置权限标识,守卫统一拦截。

原创文章,作者:小编,如若转载,请注明出处:https://www.yunthe.com/vue3-zu-he-shi-api-xiang-mu-jia-gou-shi-zhan-cong-jiao-shou/

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

相关推荐