Vue 3 + Pinia + Vite 项目架构设计:现代前端工程化实践指南

Ian266
Ian266 2026-02-12T21:12:11+08:00
0 0 0

前言

随着前端技术的快速发展,现代前端应用的架构设计变得越来越重要。Vue 3作为当前主流的前端框架,结合Pinia状态管理库和Vite构建工具,为开发者提供了强大的技术栈组合。本文将深入探讨如何基于Vue 3 + Pinia + Vite构建现代化的前端项目架构,涵盖从基础配置到高级实践的完整指南。

项目基础配置

1.1 环境准备

在开始项目搭建之前,确保开发环境已经准备就绪:

# 检查Node.js版本(建议16+)
node --version

# 检查npm版本
npm --version

# 创建项目目录
mkdir vue3-pinia-vite-app
cd vue3-pinia-vite-app

1.2 项目初始化

使用Vite快速创建Vue 3项目:

# 使用npm创建项目
npm create vite@latest . -- --template vue

# 或使用yarn
yarn create vite . --template vue

# 安装依赖
npm install

1.3 项目结构规划

一个良好的项目结构是工程化的基础:

src/
├── assets/                 # 静态资源
│   ├── images/
│   ├── styles/
│   └── fonts/
├── components/             # 公共组件
│   ├── common/
│   ├── layout/
│   └── ui/
├── composables/            # 可复用逻辑
├── hooks/                  # 自定义Hook
├── views/                  # 页面组件
├── stores/                 # 状态管理
│   ├── modules/
│   └── index.ts
├── router/                 # 路由配置
├── services/               # API服务
├── utils/                  # 工具函数
├── layouts/                # 布局组件
├── App.vue                 # 根组件
└── main.ts                 # 入口文件

Vite构建配置详解

2.1 基础配置文件

// 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'),
      '@components': resolve(__dirname, 'src/components'),
      '@views': resolve(__dirname, 'src/views'),
      '@stores': resolve(__dirname, 'src/stores'),
      '@services': resolve(__dirname, 'src/services'),
      '@utils': resolve(__dirname, 'src/utils')
    }
  },
  server: {
    port: 3000,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    outDir: 'dist',
    assetsDir: 'assets',
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus', '@element-plus/icons-vue']
        }
      }
    }
  }
})

2.2 环境变量配置

# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=Vue 3 App
VITE_APP_DEBUG=true

# .env.production
VITE_API_BASE_URL=https://api.yourapp.com
VITE_APP_TITLE=Vue 3 Production App
VITE_APP_DEBUG=false

2.3 TypeScript配置

// tsconfig.json
{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true,
    "baseUrl": ".",
    "paths": {
      "@/*": ["src/*"],
      "@components/*": ["src/components/*"],
      "@views/*": ["src/views/*"],
      "@stores/*": ["src/stores/*"],
      "@services/*": ["src/services/*"],
      "@utils/*": ["src/utils/*"]
    }
  },
  "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"]
}

Vue 3组件设计模式

3.1 组件分类与设计原则

在Vue 3项目中,我们遵循以下组件分类原则:

公共组件(Common Components)

<!-- src/components/common/Button.vue -->
<template>
  <button 
    :class="['btn', `btn--${type}`, { 'btn--disabled': disabled }]"
    :disabled="disabled"
    @click="handleClick"
  >
    <slot></slot>
  </button>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'

interface Props {
  type?: 'primary' | 'secondary' | 'danger'
  disabled?: boolean
}

interface Emits {
  (e: 'click', event: MouseEvent): void
}

const props = withDefaults(defineProps<Props>(), {
  type: 'primary',
  disabled: false
})

const emit = defineEmits<Emits>()

const handleClick = (event: MouseEvent) => {
  emit('click', event)
}
</script>

<style scoped>
.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  transition: all 0.3s;
}

.btn--primary {
  background-color: #007bff;
  color: white;
}

.btn--secondary {
  background-color: #6c757d;
  color: white;
}

.btn--danger {
  background-color: #dc3545;
  color: white;
}

.btn--disabled {
  opacity: 0.6;
  cursor: not-allowed;
}
</style>

布局组件(Layout Components)

<!-- src/components/layout/Header.vue -->
<template>
  <header class="header">
    <div class="header__container">
      <div class="header__logo">
        <router-link to="/">My App</router-link>
      </div>
      <nav class="header__nav">
        <router-link 
          v-for="item in navItems" 
          :key="item.path"
          :to="item.path"
          :class="{ 'router-link-active': $route.path === item.path }"
        >
          {{ item.name }}
        </router-link>
      </nav>
      <div class="header__user">
        <UserAvatar :user="currentUser" />
      </div>
    </div>
  </header>
</template>

<script setup lang="ts">
import { ref, computed } from 'vue'
import { useRoute } from 'vue-router'
import UserAvatar from './UserAvatar.vue'

const route = useRoute()
const currentUser = ref({ name: 'John Doe', avatar: '/avatar.jpg' })

const navItems = computed(() => [
  { path: '/', name: '首页' },
  { path: '/products', name: '产品' },
  { path: '/about', name: '关于' }
])
</script>

<style scoped>
.header {
  background-color: #fff;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  position: sticky;
  top: 0;
  z-index: 100;
}

.header__container {
  display: flex;
  justify-content: space-between;
  align-items: center;
  max-width: 1200px;
  margin: 0 auto;
  padding: 0 20px;
  height: 60px;
}

.header__logo a {
  font-size: 1.5rem;
  font-weight: bold;
  text-decoration: none;
  color: #333;
}

.header__nav {
  display: flex;
  gap: 20px;
}

.header__nav a {
  text-decoration: none;
  color: #666;
  padding: 8px 12px;
  border-radius: 4px;
  transition: all 0.3s;
}

.header__nav a.router-link-active {
  background-color: #007bff;
  color: white;
}
</style>

3.2 组件通信模式

Props + Events 通信

<!-- src/components/Modal.vue -->
<template>
  <div v-if="visible" class="modal-overlay" @click="handleOverlayClick">
    <div class="modal-content" @click.stop>
      <div class="modal-header">
        <h3>{{ title }}</h3>
        <button class="modal-close" @click="handleClose">
          ×
        </button>
      </div>
      <div class="modal-body">
        <slot></slot>
      </div>
      <div class="modal-footer">
        <Button type="secondary" @click="handleCancel">取消</Button>
        <Button type="primary" @click="handleConfirm">确定</Button>
      </div>
    </div>
  </div>
</template>

<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
import Button from './common/Button.vue'

interface Props {
  visible: boolean
  title?: string
}

interface Emits {
  (e: 'update:visible', visible: boolean): void
  (e: 'confirm'): void
  (e: 'cancel'): void
}

const props = withDefaults(defineProps<Props>(), {
  title: '提示'
})

const emit = defineEmits<Emits>()

const handleClose = () => {
  emit('update:visible', false)
}

const handleOverlayClick = () => {
  emit('update:visible', false)
}

const handleCancel = () => {
  emit('cancel')
  handleClose()
}

const handleConfirm = () => {
  emit('confirm')
  handleClose()
}
</script>

<style scoped>
.modal-overlay {
  position: fixed;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background-color: rgba(0, 0, 0, 0.5);
  display: flex;
  justify-content: center;
  align-items: center;
  z-index: 1000;
}

.modal-content {
  background-color: white;
  border-radius: 8px;
  box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
  width: 90%;
  max-width: 500px;
  max-height: 80vh;
  overflow-y: auto;
}

.modal-header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  padding: 20px;
  border-bottom: 1px solid #eee;
}

.modal-body {
  padding: 20px;
}

.modal-footer {
  display: flex;
  justify-content: flex-end;
  gap: 10px;
  padding: 20px;
  border-top: 1px solid #eee;
}

.modal-close {
  background: none;
  border: none;
  font-size: 24px;
  cursor: pointer;
  padding: 0;
  width: 30px;
  height: 30px;
  display: flex;
  align-items: center;
  justify-content: center;
}
</style>

Pinia状态管理实践

4.1 Store基础结构

// src/stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia

4.2 用户状态管理

// src/stores/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { login, logout, getUserInfo } from '@/services/auth'
import type { User } from '@/types/user'

export const useUserStore = defineStore('user', () => {
  const user = ref<User | null>(null)
  const token = ref<string | null>(localStorage.getItem('token'))
  const loading = ref(false)
  const error = ref<string | null>(null)

  const isLoggedIn = computed(() => !!user.value && !!token.value)
  const userName = computed(() => user.value?.name || '')
  const userRole = computed(() => user.value?.role || '')

  const loginAction = async (credentials: { username: string; password: string }) => {
    try {
      loading.value = true
      error.value = null
      
      const response = await login(credentials)
      const { token: newToken, user: userData } = response
      
      token.value = newToken
      user.value = userData
      
      // 保存token到localStorage
      localStorage.setItem('token', newToken)
      
      return { success: true }
    } catch (err) {
      error.value = '登录失败,请检查用户名和密码'
      return { success: false, error: error.value }
    } finally {
      loading.value = false
    }
  }

  const logoutAction = () => {
    token.value = null
    user.value = null
    localStorage.removeItem('token')
  }

  const fetchUserInfo = async () => {
    if (!token.value) return
    
    try {
      const userData = await getUserInfo()
      user.value = userData
    } catch (err) {
      console.error('获取用户信息失败:', err)
      logoutAction()
    }
  }

  return {
    user,
    token,
    loading,
    error,
    isLoggedIn,
    userName,
    userRole,
    loginAction,
    logoutAction,
    fetchUserInfo
  }
})

4.3 全局状态管理

// src/stores/modules/app.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export const useAppStore = defineStore('app', () => {
  const sidebarCollapsed = ref(false)
  const theme = ref<'light' | 'dark'>('light')
  const loading = ref(false)
  const notifications = ref<any[]>([])

  const isSidebarCollapsed = computed(() => sidebarCollapsed.value)
  const isDarkTheme = computed(() => theme.value === 'dark')

  const toggleSidebar = () => {
    sidebarCollapsed.value = !sidebarCollapsed.value
  }

  const setTheme = (newTheme: 'light' | 'dark') => {
    theme.value = newTheme
    document.documentElement.setAttribute('data-theme', newTheme)
  }

  const showLoading = () => {
    loading.value = true
  }

  const hideLoading = () => {
    loading.value = false
  }

  const addNotification = (notification: any) => {
    notifications.value.push({
      id: Date.now(),
      ...notification,
      timestamp: new Date()
    })
  }

  const removeNotification = (id: number) => {
    const index = notifications.value.findIndex(n => n.id === id)
    if (index > -1) {
      notifications.value.splice(index, 1)
    }
  }

  return {
    sidebarCollapsed,
    theme,
    loading,
    notifications,
    isSidebarCollapsed,
    isDarkTheme,
    toggleSidebar,
    setTheme,
    showLoading,
    hideLoading,
    addNotification,
    removeNotification
  }
})

4.4 复合状态管理

// src/stores/modules/products.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { getProducts, getProductById, createProduct, updateProduct, deleteProduct } from '@/services/products'
import type { Product } from '@/types/product'

export const useProductStore = defineStore('products', () => {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  const currentProduct = ref<Product | null>(null)
  const filters = ref({
    page: 1,
    limit: 10,
    search: '',
    category: ''
  })

  const total = computed(() => products.value.length)
  const paginatedProducts = computed(() => {
    const start = (filters.value.page - 1) * filters.value.limit
    return products.value.slice(start, start + filters.value.limit)
  })

  const fetchProducts = async () => {
    try {
      loading.value = true
      error.value = null
      
      const response = await getProducts({
        page: filters.value.page,
        limit: filters.value.limit,
        search: filters.value.search,
        category: filters.value.category
      })
      
      products.value = response.data
      return response
    } catch (err) {
      error.value = '获取产品列表失败'
      console.error('获取产品列表失败:', err)
      throw err
    } finally {
      loading.value = false
    }
  }

  const fetchProductById = async (id: number) => {
    try {
      loading.value = true
      const product = await getProductById(id)
      currentProduct.value = product
      return product
    } catch (err) {
      error.value = '获取产品详情失败'
      throw err
    } finally {
      loading.value = false
    }
  }

  const createProductAction = async (productData: Partial<Product>) => {
    try {
      loading.value = true
      const newProduct = await createProduct(productData)
      products.value.push(newProduct)
      return newProduct
    } catch (err) {
      error.value = '创建产品失败'
      throw err
    } finally {
      loading.value = false
    }
  }

  const updateProductAction = async (id: number, productData: Partial<Product>) => {
    try {
      loading.value = true
      const updatedProduct = await updateProduct(id, productData)
      const index = products.value.findIndex(p => p.id === id)
      if (index > -1) {
        products.value[index] = updatedProduct
      }
      return updatedProduct
    } catch (err) {
      error.value = '更新产品失败'
      throw err
    } finally {
      loading.value = false
    }
  }

  const deleteProductAction = async (id: number) => {
    try {
      loading.value = true
      await deleteProduct(id)
      const index = products.value.findIndex(p => p.id === id)
      if (index > -1) {
        products.value.splice(index, 1)
      }
      return true
    } catch (err) {
      error.value = '删除产品失败'
      throw err
    } finally {
      loading.value = false
    }
  }

  const setFilters = (newFilters: Partial<typeof filters.value>) => {
    Object.assign(filters.value, newFilters)
  }

  const resetFilters = () => {
    filters.value = {
      page: 1,
      limit: 10,
      search: '',
      category: ''
    }
  }

  return {
    products,
    loading,
    error,
    currentProduct,
    filters,
    total,
    paginatedProducts,
    fetchProducts,
    fetchProductById,
    createProductAction,
    updateProductAction,
    deleteProductAction,
    setFilters,
    resetFilters
  }
})

API服务层设计

5.1 Axios封装

// src/services/api.ts
import axios, { type AxiosInstance, type AxiosRequestConfig, type AxiosResponse } from 'axios'
import { useUserStore } from '@/stores/modules/user'
import { ElMessage } from 'element-plus'

// 创建axios实例
const apiClient: AxiosInstance = axios.create({
  baseURL: import.meta.env.VITE_API_BASE_URL,
  timeout: 10000,
  headers: {
    'Content-Type': 'application/json'
  }
})

// 请求拦截器
apiClient.interceptors.request.use(
  (config: AxiosRequestConfig) => {
    const userStore = useUserStore()
    const token = userStore.token
    
    if (token) {
      config.headers = {
        ...config.headers,
        Authorization: `Bearer ${token}`
      }
    }
    
    return config
  },
  (error) => {
    return Promise.reject(error)
  }
)

// 响应拦截器
apiClient.interceptors.response.use(
  (response: AxiosResponse) => {
    return response.data
  },
  (error) => {
    if (error.response?.status === 401) {
      const userStore = useUserStore()
      userStore.logoutAction()
      ElMessage.error('登录已过期,请重新登录')
      window.location.href = '/login'
    } else if (error.response?.status === 403) {
      ElMessage.error('权限不足')
    } else if (error.response?.status >= 500) {
      ElMessage.error('服务器内部错误')
    } else {
      ElMessage.error(error.response?.data?.message || '请求失败')
    }
    
    return Promise.reject(error)
  }
)

export default apiClient

5.2 服务模块

// src/services/auth.ts
import apiClient from './api'
import type { User } from '@/types/user'

interface LoginResponse {
  token: string
  user: User
}

interface LoginCredentials {
  username: string
  password: string
}

export const login = async (credentials: LoginCredentials): Promise<LoginResponse> => {
  const response = await apiClient.post('/auth/login', credentials)
  return response
}

export const logout = async (): Promise<void> => {
  await apiClient.post('/auth/logout')
}

export const getUserInfo = async (): Promise<User> => {
  const response = await apiClient.get('/auth/me')
  return response
}

export const register = async (userData: any): Promise<User> => {
  const response = await apiClient.post('/auth/register', userData)
  return response
}
// src/services/products.ts
import apiClient from './api'
import type { Product } from '@/types/product'

interface ProductFilters {
  page?: number
  limit?: number
  search?: string
  category?: string
}

interface ProductListResponse {
  data: Product[]
  total: number
  page: number
  limit: number
}

export const getProducts = async (filters: ProductFilters): Promise<ProductListResponse> => {
  const response = await apiClient.get('/products', { params: filters })
  return response
}

export const getProductById = async (id: number): Promise<Product> => {
  const response = await apiClient.get(`/products/${id}`)
  return response
}

export const createProduct = async (productData: Partial<Product>): Promise<Product> => {
  const response = await apiClient.post('/products', productData)
  return response
}

export const updateProduct = async (id: number, productData: Partial<Product>): Promise<Product> => {
  const response = await apiClient.put(`/products/${id}`, productData)
  return response
}

export const deleteProduct = async (id: number): Promise<void> => {
  await apiClient.delete(`/products/${id}`)
}

路由配置与权限管理

6.1 路由基础配置

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

// 路由元信息类型
interface RouteMeta {
  requiresAuth?: boolean
  title?: string
  icon?: string
  permission?: string
}

// 定义路由组件
const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/Home.vue'),
    meta: { title: '首页' }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('@/views/auth/Login.vue'),
    meta: { title: '登录' }
  },
  {
    path: '/products',
    name: 'Products',
    component: () => import('@/views/products/Products.vue'),
    meta: { 
      title: '产品管理',
      requiresAuth: true,
      permission: 'products:view'
    }
  },
  {
    path: '/products/create',
    name: 'ProductCreate',
    component: () => import('@/views/products/Create.vue'),
    meta: { 
      title: '创建产品',
      requiresAuth: true,
      permission: 'products:create'
    }
  },
  {
    path: '/products/:id/edit',
    name: 'ProductEdit',
    component: () => import('@/views/products/Edit.vue'),
    meta: { 
      title: '编辑产品',
      requiresAuth: true,
      permission: 'products:edit'
    },
    props: true
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
    meta: { title: '页面未找到' }
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  const requiresAuth = to.meta.requiresAuth
  
  if (requiresAuth && !userStore.isLoggedIn) {
    next({
      path: '/login',
      query: { redirect: to.fullPath }
    })
  } else if (to.path === '/login' && userStore.isLoggedIn) {
    next('/')
  } else {
    next()
  }
})

export default router

6.2 权限管理

// src/utils/permission.ts
import { useUserStore } from '@/stores/modules/user'

export const hasPermission = (permission: string): boolean => {
  const userStore = useUserStore()
  const userRole = userStore.userRole
  
  // 简单的权限检查逻辑
  const permissionsMap: Record<string, string[]> = {
    admin: ['products:view', 'products:create', 'products:edit', 'products:delete'],
    manager: ['products:view', 'products:create', 'products:edit'],
    user: ['products:view']
  }
  
  const userPermissions = permissionsMap[userRole] || []
  return userPermissions.includes(permission)
}

export const checkPermission = (permission: string): boolean => {
  return hasPermission(permission)
}

代码规范与最佳实践

7.1 ESLint配置

// .eslintrc.json
{
  "env": {
    "browser": true,
    "es2021": true,
    "node": true
  },
  "extends": [
    "eslint:recommended",
    "@typescript-eslint/recommended",
    "plugin:vue/vue3-essential",
    "plugin:vue/vue3-strongly-recommended",
    "plugin:vue/vue3-recommended"
  ],
  "parserOptions": {
    "ecmaVersion": "latest",
    "parser": "@typescript-eslint/parser",
    "sourceType
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000