前言
随着前端技术的快速发展,现代前端应用的架构设计变得越来越重要。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)