Vue3组合式API企业级项目架构实战:Pinia + Vue Router + TypeScript完整解决方案

随着Vue3的普及,组合式API已经成为企业级项目的首选开发方式。本文将详细介绍如何使用Vue3 + TypeScript + Pinia + Vue Router搭建一套完整的企业级项目架构,帮助你快速上手并应用到实际项目中。

一、项目初始化与目录结构

首先使用Vite创建项目,选择Vue + TypeScript模板:

npm create vite@latest my-project -- --template vue-ts
cd my-project
npm install

企业级项目推荐目录结构:

src/
├── api/              # API接口封装
├── assets/           # 静态资源
├── components/       # 公共组件
│   ├── common/       # 通用组件
│   └── business/     # 业务组件
├── composables/      # 组合式函数
├── directives/       # 自定义指令
├── hooks/            # 自定义hooks
├── router/           # 路由配置
├── stores/           # Pinia状态管理
├── styles/           # 全局样式
├── types/            # TypeScript类型定义
├── utils/            # 工具函数
└── views/            # 页面组件

二、Pinia状态管理最佳实践

Pinia是Vue3官方推荐的状态管理库,相比Vuex更加轻量和类型友好。

2.1 安装与配置

npm install pinia

在main.ts中配置:

// main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)
const pinia = createPinia()

app.use(pinia)
app.use(router)
app.mount('#app')

2.2 定义用户状态Store

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

interface UserInfo {
  id: number
  username: string
  email: string
  avatar: string
  roles: string[]
}

export const useUserStore = defineStore('user', () => {
  // 状态
  const token = ref(localStorage.getItem('token') || '')
  const userInfo = ref(null)

  // 计算属性
  const isLoggedIn = computed(() => !!token.value)
  const username = computed(() => userInfo.value?.username || '游客')

  // 方法
  function setToken(newToken: string) {
    token.value = newToken
    localStorage.setItem('token', newToken)
  }

  function setUserInfo(info: UserInfo) {
    userInfo.value = info
  }

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

  return {
    token,
    userInfo,
    isLoggedIn,
    username,
    setToken,
    setUserInfo,
    logout
  }
})

2.3 定义异步请求Store

// stores/counter.ts
import { defineStore } from 'pinia'
import { ref } from 'vue'

export const useCounterStore = defineStore('counter', () => {
  const count = ref(0)
  const loading = ref(false)

  async function increment() {
    loading.value = true
    try {
      // 模拟API请求
      await new Promise(resolve => setTimeout(resolve, 500))
      count.value++
    } finally {
      loading.value = false
    }
  }

  async function fetchCount() {
    loading.value = true
    try {
      const response = await fetch('/api/count')
      const data = await response.json()
      count.value = data.count
    } finally {
      loading.value = false
    }
  }

  return { count, loading, increment, fetchCount }
})

三、Vue Router路由配置

3.1 基础路由配置

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

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页' }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/Login.vue'),
    meta: { title: '登录', noAuth: true }
  },
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: () => import('@/views/Dashboard.vue'),
    meta: { title: '控制台', requiresAuth: true }
  },
  {
    path: '/user',
    name: 'User',
    component: () => import('@/views/user/index.vue'),
    meta: { title: '用户管理', requiresAuth: true },
    children: [
      {
        path: 'list',
        name: 'UserList',
        component: () => import('@/views/user/List.vue'),
        meta: { title: '用户列表' }
      },
      {
        path: 'detail/:id',
        name: 'UserDetail',
        component: () => import('@/views/user/Detail.vue'),
        meta: { title: '用户详情' }
      }
    ]
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
    meta: { title: '页面不存在', noAuth: true }
  }
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    return { top: 0 }
  }
})

// 路由守卫
router.beforeEach((to, from, next) => {
  // 设置页面标题
  document.title = `${to.meta.title || 'Vue3 App'} - xjahb.cn`
  
  const userStore = useUserStore()
  
  // 需要登录但未登录
  if (to.meta.requiresAuth && !userStore.isLoggedIn) {
    next({ name: 'Login', query: { redirect: to.fullPath } })
    return
  }
  
  // 已登录访问登录页
  if (to.name === 'Login' && userStore.isLoggedIn) {
    next({ name: 'Home' })
    return
  }
  
  next()
})

export default router

3.2 动态路由添加

// router/dynamic.ts
import router from './index'
import { useUserStore } from '@/stores/user'

// 根据角色动态添加路由
export function addDynamicRoutes() {
  const userStore = useUserStore()
  const roles = userStore.userInfo?.roles || []

  const adminRoutes: RouteRecordRaw[] = [
    {
      path: '/admin',
      name: 'Admin',
      component: () => import('@/views/admin/index.vue'),
      meta: { title: '后台管理' },
      children: [
        {
          path: 'settings',
          name: 'Settings',
          component: () => import('@/views/admin/Settings.vue'),
          meta: { title: '系统设置' }
        }
      ]
    }
  ]

  if (roles.includes('admin')) {
    adminRoutes.forEach(route => router.addRoute(route))
  }
}

四、TypeScript类型定义规范

4.1 API响应类型

// types/api.ts

// 通用API响应结构
interface ApiResponse {
  code: number
  message: string
  data: T
}

// 分页数据结构
interface PageData {
  list: T[]
  total: number
  page: number
  pageSize: number
}

// 用户相关类型
interface User {
  id: number
  username: string
  email: string
  avatar: string
  status: 0 | 1  // 0: 禁用, 1: 启用
  createTime: string
  updateTime: string
}

// 登录请求参数
interface LoginParams {
  username: string
  password: string
  captcha?: string
}

// 登录响应
interface LoginResponse {
  token: string
  userInfo: User
}

export type {
  ApiResponse,
  PageData,
  User,
  LoginParams,
  LoginResponse
}

4.2 组件Props类型

// components/common/UserCard.vue


五、组合式函数(Composables)封装

5.1 通用请求Hook

// composables/useRequest.ts
import { ref, Ref } from 'vue'

interface UseRequestOptions {
  immediate?: boolean
  initialData?: T
  onSuccess?: (data: T) => void
  onError?: (error: Error) => void
}

export function useRequest(
  requestFn: () => Promise,
  options: UseRequestOptions = {}
) {
  const { immediate = false, initialData, onSuccess, onError } = options

  const data: Ref = ref(initialData)
  const loading = ref(false)
  const error = ref(null)

  async function execute() {
    loading.value = true
    error.value = null
    try {
      const result = await requestFn()
      data.value = result
      onSuccess?.(result)
      return result
    } catch (e) {
      error.value = e as Error
      onError?.(e as Error)
      throw e
    } finally {
      loading.value = false
    }
  }

  if (immediate) {
    execute()
  }

  return {
    data,
    loading,
    error,
    execute
  }
}

5.2 分页Hook

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

interface UsePaginationOptions {
  pageSize?: number
  total?: number
}

export function usePagination(options: UsePaginationOptions = {}) {
  const { pageSize = 10, total = 0 } = options

  const currentPage = ref(1)
  const pageSizeRef = ref(pageSize)
  const totalRef = ref(total)

  const totalPages = computed(() => 
    Math.ceil(totalRef.value / pageSizeRef.value)
  )

  const isFirstPage = computed(() => currentPage.value === 1)
  const isLastPage = computed(() => currentPage.value === totalPages.value)

  function goToPage(page: number) {
    if (page >= 1 && page <= totalPages.value) {
      currentPage.value = page
    }
  }

  function prevPage() {
    if (!isFirstPage.value) {
      currentPage.value--
    }
  }

  function nextPage() {
    if (!isLastPage.value) {
      currentPage.value++
    }
  }

  function setTotal(newTotal: number) {
    totalRef.value = newTotal
  }

  return {
    currentPage,
    pageSize: pageSizeRef,
    total: totalRef,
    totalPages,
    isFirstPage,
    isLastPage,
    goToPage,
    prevPage,
    nextPage,
    setTotal
  }
}

六、API封装与拦截器

// api/request.ts
import { useUserStore } from '@/stores/user'

const BASE_URL = '/api'

interface RequestConfig {
  method?: 'GET' | 'POST' | 'PUT' | 'DELETE'
  headers?: Record
  body?: any
}

async function request(
  url: string,
  config: RequestConfig = {}
): Promise {
  const { method = 'GET', headers = {}, body } = config
  const userStore = useUserStore()

  const defaultHeaders: Record = {
    'Content-Type': 'application/json',
    ...headers
  }

  // 添加认证token
  if (userStore.token) {
    defaultHeaders['Authorization'] = `Bearer ${userStore.token}`
  }

  const options: RequestInit = {
    method,
    headers: defaultHeaders
  }

  if (body && method !== 'GET') {
    options.body = JSON.stringify(body)
  }

  const response = await fetch(`${BASE_URL}${url}`, options)
  
  if (!response.ok) {
    if (response.status === 401) {
      userStore.logout()
      window.location.href = '/login'
    }
    throw new Error(`HTTP error! status: ${response.status}`)
  }

  const data = await response.json()
  
  if (data.code !== 0) {
    throw new Error(data.message || '请求失败')
  }

  return data.data
}

// 导出便捷方法
export const api = {
  get: (url: string, config?: RequestConfig) => 
    request(url, { ...config, method: 'GET' }),
  post: (url: string, body?: any, config?: RequestConfig) => 
    request(url, { ...config, method: 'POST', body }),
  put: (url: string, body?: any, config?: RequestConfig) => 
    request(url, { ...config, method: 'PUT', body }),
  delete: (url: string, config?: RequestConfig) => 
    request(url, { ...config, method: 'DELETE' })
}

七、实战示例:用户列表页面

// views/user/List.vue




八、项目配置优化

8.1 Vite配置

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'

export default defineConfig({
  plugins: [vue()],
  resolve: {
    alias: {
      '@': resolve(__dirname, 'src')
    }
  },
  server: {
    port: 3000,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true
      }
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vue-vendor': ['vue', 'vue-router', 'pinia'],
          'ui-vendor': ['element-plus']
        }
      }
    }
  }
})

8.2 TypeScript配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "moduleResolution": "bundler",
    "allowImportingTsExtensions": true,
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": true,
    "jsx": "preserve",
    "strict": true,
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noFallthroughCasesInSwitch": true,
    "paths": {
      "@/*": ["./src/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
  "references": [{ "path": "./tsconfig.node.json" }]
}

总结

本文详细介绍了Vue3企业级项目的完整架构方案,包括:

  • 项目结构:清晰的目录划分,便于团队协作
  • Pinia状态管理:类型安全的响应式状态管理
  • Vue Router:完整的路由配置和权限控制
  • TypeScript类型:API、组件Props等类型定义规范
  • Composables:可复用的组合式函数封装
  • API封装:统一的请求处理和拦截器

这套架构已在多个企业级项目中验证,能够有效提升开发效率和代码质量。希望本文对你的Vue3项目开发有所帮助!

更多前端技术文章,请访问 xjahb.cn

© 版权声明
THE END
喜欢就支持一下吧
点赞9 分享
评论 抢沙发
头像
欢迎您留下宝贵的见解!
提交
头像

昵称

取消
昵称表情代码图片快捷回复

    暂无评论内容