Vue 3 + TypeScript 项目架构实践
1. 引言
Vue 3 + TypeScript + Vite + Pinia 技术栈已经成为现代前端开发的主流选择,其优秀的性能和开发体验备受开发者青睐。然而,要充分发挥这一技术栈的潜力,合理的项目架构设计至关重要。
一个良好的项目架构能够:
- 提高代码的可维护性和可扩展性
- 减少团队协作中的冲突和问题
- 加速开发效率和代码质量
- 便于测试和部署
本文将详细介绍 Vue 3 + TypeScript 项目的架构实践,包括项目结构设计、状态管理架构、路由架构、API 层设计、工具类和公共组件的组织,以及企业级项目案例分析。
2. 项目结构设计
2.1 目录结构组织
一个合理的目录结构是项目架构的基础,它能够清晰地分离不同功能模块,便于开发者理解和维护代码。
// 推荐的 Vue 3 + TypeScript 项目目录结构
// 该结构清晰分离了不同功能模块,便于开发者理解和维护代码
// 每个目录都有明确的职责和用途,遵循关注点分离原则
// 设计意图:通过模块化组织提高代码可维护性和可扩展性
src/ // 源代码目录
├── assets/ # 静态资源文件
│ ├── images/ # 图片资源(如产品图片、背景图等)
│ ├── styles/ # 全局样式(如重置样式、主题样式等)
│ └── icons/ # 图标资源(如 SVG 图标、字体图标等)
├── components/ # 公共组件
│ ├── common/ # 通用基础组件(如按钮、输入框等)
│ ├── layout/ # 布局相关组件(如头部、侧边栏等)
│ └── business/ # 业务相关组件(如用户卡片、产品列表等)
├── composables/ # 组合式 API 逻辑
│ ├── useAuth.ts # 认证相关逻辑(如登录、注册等)
│ ├── useApi.ts # API 调用逻辑(如请求封装、错误处理等)
│ └── useLocalStorage.ts # 本地存储逻辑(如数据持久化等)
├── constants/ # 常量定义
│ ├── api.ts # API 相关常量(如接口地址、请求超时等)
│ ├── routes.ts # 路由相关常量(如路由名称、路径等)
│ └── storage.ts # 存储相关常量(如存储键名、过期时间等)
├── enums/ # 枚举类型定义
│ ├── user.ts # 用户相关枚举(如用户角色、状态等)
│ └── status.ts # 状态相关枚举(如订单状态、审批状态等)
├── hooks/ # 自定义钩子(与 composables 类似,可根据团队习惯选择其一)
├── layouts/ # 布局组件
│ ├── DefaultLayout.vue # 默认布局(如包含侧边栏和头部的布局)
│ └── AuthLayout.vue # 认证布局(如登录、注册页面的布局)
├── models/ # 数据模型定义
│ ├── user.ts # 用户模型(如用户信息结构、类型定义等)
│ ├── product.ts # 产品模型(如产品信息结构、类型定义等)
│ └── common.ts # 通用模型(如分页结构、响应结构等)
├── router/ # 路由配置
│ ├── index.ts # 路由主配置(如创建路由实例、应用守卫等)
│ ├── routes.ts # 路由定义(如路由路径、组件映射等)
│ └── guards.ts # 路由守卫(如认证守卫、权限守卫等)
├── services/ # 服务层
│ ├── api/ # API 服务
│ │ ├── user.ts # 用户相关 API(如登录、获取用户信息等)
│ │ ├── product.ts # 产品相关 API(如获取产品列表、创建产品等)
│ │ └── index.ts # API 服务主文件(如创建 axios 实例、配置拦截器等)
│ └── utils/ # 工具服务(如第三方服务集成、业务工具等)
├── stores/ # Pinia 状态管理
│ ├── user.ts # 用户状态(如用户信息、认证状态等)
│ ├── product.ts # 产品状态(如产品列表、详情等)
│ └── common.ts # 通用状态(如全局加载状态、错误信息等)
├── types/ # TypeScript 类型定义
│ ├── api.ts # API 相关类型(如请求参数、响应类型等)
│ ├── components.ts # 组件相关类型(如 props 类型、事件类型等)
│ └── common.ts # 通用类型(如通用接口、工具类型等)
├── utils/ # 工具函数
│ ├── format.ts # 格式化工具(如日期格式化、金额格式化等)
│ ├── validation.ts # 验证工具(如邮箱验证、密码强度验证等)
│ └── storage.ts # 存储工具(如本地存储封装、会话存储封装等)
├── views/ # 页面组件
│ ├── auth/ # 认证相关页面
│ │ ├── Login.vue # 登录页面
│ │ └── Register.vue # 注册页面
│ ├── dashboard/ # 仪表盘页面
│ └── products/ # 产品相关页面
├── App.vue # 根组件(应用的入口组件)
├── main.ts # 应用入口(如初始化 Vue 应用、注册插件等)
└── env.d.ts # 环境变量类型定义(如 Vite 环境变量类型声明)
2.2 目录结构设计原则
- 功能模块化:将相关功能的代码组织在一起,便于理解和维护
- 关注点分离:将不同职责的代码分离到不同目录,如视图、组件、状态管理等
- 层次清晰:建立清晰的代码层次结构,如 API 层、服务层、业务逻辑层等
- 可扩展性:预留合理的扩展空间,便于后续功能的添加和修改
- 命名规范:采用一致的命名规范,提高代码的可读性
2.3 模块划分策略
按业务功能划分
将代码按照业务功能进行划分,每个功能模块包含完整的组件、状态、服务等。
// 按业务功能划分的目录结构
// 该结构将代码按照业务功能进行划分,每个功能模块包含完整的组件、状态、服务等
// 优点:业务逻辑内聚,便于团队协作和代码维护
// 缺点:可能导致某些通用代码重复
// 适用场景:大型应用,多个团队负责不同业务模块
// 设计意图:通过业务模块划分,实现团队协作的隔离和业务逻辑的内聚
src/
├── modules/ # 业务模块目录(存放各个独立的业务模块)
│ ├── auth/ # 认证模块(处理登录、注册等认证相关功能)
│ │ ├── components/ # 认证相关组件(如登录表单、注册表单等)
│ │ ├── views/ # 认证相关页面(如登录页面、注册页面等)
│ │ ├── services/ # 认证相关服务(如认证 API 调用等)
│ │ └── stores/ # 认证相关状态(如认证状态管理等)
│ ├── product/ # 产品模块(处理产品相关功能)
│ │ ├── components/ # 产品相关组件(如产品卡片、产品列表项等)
│ │ ├── views/ # 产品相关页面(如产品列表页、产品详情页等)
│ │ ├── services/ # 产品相关服务(如产品 API 调用等)
│ │ └── stores/ # 产品相关状态(如产品列表状态、详情状态等)
│ └── user/ # 用户模块(处理用户相关功能)
│ ├── components/ # 用户相关组件(如用户信息卡片、用户列表项等)
│ ├── views/ # 用户相关页面(如用户列表页、用户详情页等)
│ ├── services/ # 用户相关服务(如用户 API 调用等)
│ └── stores/ # 用户相关状态(如用户列表状态、详情状态等)
└── shared/ # 共享资源(存放各个模块共用的代码)
├── components/ # 共享组件(如通用按钮、输入框等)
├── composables/ # 共享组合式 API(如通用认证逻辑、API 调用逻辑等)
├── utils/ # 共享工具函数(如格式化工具、验证工具等)
└── types/ # 共享类型定义(如通用接口、类型声明等)
按技术类型划分
将代码按照技术类型进行划分,如组件、服务、状态管理等。
// 按技术类型划分的目录结构
// 该结构将代码按照技术类型进行划分,如组件、服务、状态管理等
// 优点:技术职责清晰,便于代码复用和维护
// 缺点:业务逻辑分散在不同目录,可能增加跨模块理解难度
// 适用场景:中小型应用,团队成员技术栈全面
// 设计意图:通过技术类型划分,实现代码的分类管理和复用
src/
├── components/ # 所有组件(按技术类型组织的组件集合)
├── services/ # 所有服务(按技术类型组织的服务集合)
├── stores/ # 所有状态管理(按技术类型组织的状态管理集合)
├── views/ # 所有页面(按技术类型组织的页面集合)
└── utils/ # 所有工具函数(按技术类型组织的工具函数集合)
混合划分策略
结合业务功能和技术类型的划分策略,在顶层按技术类型划分,在具体模块内按业务功能划分。
// 混合划分策略的目录结构
// 该结构结合业务功能和技术类型的划分策略,在顶层按技术类型划分,在具体模块内按业务功能划分
// 优点:既保持了技术职责清晰,又保证了业务逻辑的内聚性
// 缺点:目录结构可能相对复杂
// 适用场景:中大型应用,需要平衡技术管理和业务逻辑
// 设计意图:通过混合划分策略,兼顾技术管理的清晰性和业务逻辑的内聚性
src/
├── components/ # 所有组件(顶层按技术类型划分)
│ ├── common/ # 通用组件(如按钮、输入框等通用基础组件)
│ ├── auth/ # 认证相关组件(如登录表单、注册表单等)
│ └── product/ # 产品相关组件(如产品卡片、产品列表等)
├── services/ # 所有服务(顶层按技术类型划分)
│ ├── api/ # API 服务(按业务功能划分的 API 服务)
│ │ ├── auth.ts # 认证 API(处理认证相关的 API 调用)
│ │ └── product.ts # 产品 API(处理产品相关的 API 调用)
│ └── utils/ # 服务工具(如 API 请求封装、错误处理等)
└── stores/ # 所有状态管理(顶层按技术类型划分)
├── auth.ts # 认证状态(处理认证相关的状态管理)
└── product.ts # 产品状态(处理产品相关的状态管理)
2.4 推荐的目录结构
根据项目规模和团队习惯,推荐使用以下目录结构:
// 推荐的目录结构
// 该结构根据项目规模和团队习惯,综合考虑了技术管理和业务逻辑的平衡
// 每个目录都有明确的职责,便于开发者理解和维护代码
// 适用场景:大多数 Vue 3 + TypeScript 项目
// 设计意图:提供一个通用的、可扩展的目录结构,适用于各种规模的项目
src/
├── assets/ # 静态资源(如图片、样式、图标等)
├── components/ # 公共组件(如通用组件、业务组件等)
├── composables/ # 组合式 API(如认证逻辑、API 调用逻辑等)
├── constants/ # 常量定义(如 API 地址、路由名称等)
├── enums/ # 枚举类型(如用户角色、订单状态等)
├── layouts/ # 布局组件(如默认布局、认证布局等)
├── models/ # 数据模型(如用户模型、产品模型等)
├── router/ # 路由配置(如路由定义、守卫等)
├── services/ # 服务层(如 API 服务、工具服务等)
├── stores/ # Pinia 状态管理(如用户状态、产品状态等)
├── types/ # TypeScript 类型(如 API 类型、组件类型等)
├── utils/ # 工具函数(如格式化工具、验证工具等)
├── views/ # 页面组件(如认证页面、仪表盘页面等)
├── App.vue # 根组件(应用的入口组件)
├── main.ts # 入口文件(初始化 Vue 应用、注册插件等)
└── env.d.ts # 环境变量类型(Vite 环境变量类型声明)
3. 状态管理架构
3.1 Pinia 状态管理实践
Pinia 作为 Vue 3 官方推荐的状态管理库,提供了更简洁、更灵活的状态管理方案。
状态管理结构设计
- 按模块划分 Store:将状态按照业务模块进行划分,每个模块对应一个 Store
- 使用 Composition API:采用 Composition API 风格定义 Store,提高代码的可读性和可维护性
- 类型安全:充分利用 TypeScript 的类型系统,为 Store 定义明确的类型
Store 组织方式
typescript
// stores/user.ts
// 用户状态管理 Store
// 该文件使用 Pinia 的 Composition API 风格定义用户相关的状态管理
// 包含用户信息、认证状态、登录/登出等功能
// 特点:类型安全,代码结构清晰,逻辑组织合理
// 设计意图:通过 Pinia 管理用户认证状态和用户信息,实现状态的集中管理和持久化
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { User } from '@/models/user'
import { userApi } from '@/services/api/user'
/**
* 用户状态管理 Store
* 管理用户认证状态、用户信息等
* 使用 Composition API 风格定义,提供更好的类型推断和代码组织
* @returns 用户状态管理 Store 实例
*/
export const useUserStore = defineStore('user', () => {
// 状态(State)
const currentUser = ref<User | null>(null) // 当前用户信息,类型为 User 或 null
const token = ref<string | null>(null) // 认证令牌,用于 API 调用的身份验证
const loading = ref(false) // 加载状态,用于控制加载指示器的显示
const error = ref<string | null>(null) // 错误信息,用于显示错误提示
// Getters(计算属性)
const isAuthenticated = computed(() => !!token.value) // 是否已认证,根据 token 是否存在判断
const userDisplayName = computed(() => {
return currentUser.value ? `${currentUser.value.firstName} ${currentUser.value.lastName}` : ''
}) // 用户显示名称,组合 firstName 和 lastName
// Actions(方法)
/**
* 用户登录
* @param email 邮箱
* @param password 密码
* @returns 登录响应,包含用户信息和令牌
* @throws 登录失败时抛出错误
*/
async function login(email: string, password: string) {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.login({ email, password }) // 调用登录 API
currentUser.value = response.user // 更新当前用户信息
token.value = response.token // 更新认证令牌
localStorage.setItem('token', response.token) // 持久化存储令牌到 localStorage
return response // 返回登录响应
} catch (err) {
error.value = 'Login failed' // 设置错误信息
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
/**
* 用户登出
* @throws 登出失败时抛出错误
*/
async function logout() {
loading.value = true // 开始加载
try {
await userApi.logout() // 调用登出 API
currentUser.value = null // 清空当前用户信息
token.value = null // 清空认证令牌
localStorage.removeItem('token') // 清除 localStorage 中的令牌
} catch (err) {
error.value = 'Logout failed' // 设置错误信息
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
/**
* 获取当前用户信息
* @returns 用户信息
* @throws 获取失败时抛出错误
*/
async function fetchCurrentUser() {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.getCurrentUser() // 调用获取用户信息 API
currentUser.value = response // 更新当前用户信息
return response // 返回用户信息
} catch (err) {
error.value = 'Failed to fetch user' // 设置错误信息
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
/**
* 初始化用户状态
* 从本地存储恢复令牌并获取用户信息
* 在应用启动时调用,用于保持用户登录状态
*/
function initialize() {
const storedToken = localStorage.getItem('token') // 从 localStorage 获取存储的令牌
if (storedToken) {
token.value = storedToken // 更新认证令牌
fetchCurrentUser() // 获取用户信息
}
}
return {
// 状态(State)
currentUser,
token,
loading,
error,
// Getters(计算属性)
isAuthenticated,
userDisplayName,
// Actions(方法)
login,
logout,
fetchCurrentUser,
initialize
}
})
3.2 跨 Store 通信策略
在复杂的应用中,不同 Store 之间可能需要进行通信。以下是几种跨 Store 通信的策略:
1. 直接引用其他 Store
在一个 Store 中直接引用和使用另一个 Store。
typescript
// stores/cart.ts
// 购物车状态管理 Store
// 该文件演示了如何通过直接引用其他 Store 来实现跨 Store 通信
// 优点:代码简洁直接,易于理解
// 缺点:Store 之间耦合度较高
// 设计意图:演示跨 Store 通信的直接引用方法,适用于简单场景
import { defineStore } from 'pinia'
import { useUserStore } from './user'
/**
* 购物车状态管理 Store
* 管理购物车商品、数量等
* @returns 购物车状态管理 Store 实例
*/
export const useCartStore = defineStore('cart', () => {
// 直接引用用户 Store
// 优点:代码简洁直接,易于理解
// 缺点:Store 之间耦合度较高,可能导致循环依赖
const userStore = useUserStore()
/**
* 获取用户购物车
* 根据用户认证状态获取对应的购物车数据
* 演示了如何使用其他 Store 的状态(isAuthenticated)
*/
function getUserCart() {
if (userStore.isAuthenticated) {
// 获取登录用户的购物车
// 当用户已认证时,获取与用户账号关联的购物车
console.log('Getting cart for authenticated user')
} else {
// 获取访客购物车
// 当用户未认证时,获取基于本地存储的访客购物车
console.log('Getting cart for guest user')
}
}
return {
getUserCart
}
})
2. 使用事件总线
通过事件总线在不同 Store 之间传递消息。
typescript
// utils/eventBus.ts
// 事件总线工具
// 用于在不同组件和 Store 之间传递消息
// 基于 mitt 库实现
// 设计意图:通过事件总线实现组件和 Store 之间的解耦通信
import mitt from 'mitt'
/**
* 事件总线实例
* 用于跨组件和跨 Store 通信
* 优点:组件和 Store 之间解耦,减少直接依赖
* 缺点:事件流难以追踪,可能导致调试困难
*/
export const eventBus = mitt()
// stores/user.ts
// 用户状态管理 Store
// 该文件演示了如何通过事件总线发送事件
// 设计意图:登录成功后通知其他模块,实现跨模块通信
import { defineStore } from 'pinia'
import { eventBus } from '@/utils/eventBus'
/**
* 用户状态管理 Store
* @returns 用户状态管理 Store 实例
*/
export const useUserStore = defineStore('user', () => {
/**
* 用户登录
* 登录成功后通过事件总线通知其他 Store
* 演示了如何通过事件总线发送事件
*/
function login() {
// 登录逻辑
console.log('User logging in...')
// 登录成功后发送事件
// 事件名称:'user:login',事件数据:{ userId: 123 }
eventBus.emit('user:login', { userId: 123 })
}
return {
login
}
})
// stores/cart.ts
// 购物车状态管理 Store
// 该文件演示了如何通过事件总线监听事件
// 设计意图:监听用户登录事件,同步购物车数据
import { defineStore } from 'pinia'
import { eventBus } from '@/utils/eventBus'
/**
* 购物车状态管理 Store
* @returns 购物车状态管理 Store 实例
*/
export const useCartStore = defineStore('cart', () => {
/**
* 初始化购物车
* 监听用户登录事件,以便同步购物车数据
* 演示了如何通过事件总线监听事件
*/
function initialize() {
// 监听用户登录事件
// 事件名称:'user:login',回调函数处理事件数据
eventBus.on('user:login', (data) => {
// 处理用户登录事件
console.log('User logged in:', data.userId)
// 可以在这里同步购物车数据
// 例如:将访客购物车数据同步到登录用户的购物车
})
}
return {
initialize
}
})
3. 使用 Composition API 共享逻辑
将共享逻辑提取到 composables 中,供多个 Store 使用。
typescript
// composables/useApi.ts
// API 调用逻辑
// 该文件演示了如何通过 Composition API 封装共享逻辑
// 优点:逻辑复用性高,Store 之间耦合度低
// 设计意图:封装 API 请求的通用逻辑,供多个 Store 和组件复用
import { ref } from 'vue'
/**
* API 调用逻辑
* 封装了 API 请求的通用逻辑,如加载状态、错误处理等
* 使用 Composition API 风格,提供更好的代码组织和复用性
* @returns API 调用相关的状态和方法
*/
export function useApi() {
const loading = ref(false) // 加载状态,用于控制加载指示器的显示
const error = ref<string | null>(null) // 错误信息,用于显示错误提示
/**
* 发起 API 请求
* 封装了 API 请求的通用逻辑,包括加载状态管理和错误处理
* @param apiCall API 调用函数,返回 Promise
* @returns API 响应数据
* @throws API 请求失败时抛出错误
*/
async function request<T>(apiCall: () => Promise<T>): Promise<T> {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
return await apiCall() // 执行 API 调用
} catch (err) {
error.value = 'API request failed' // 设置错误信息
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
return {
loading, // 加载状态
error, // 错误信息
request // API 请求方法
}
}
// stores/user.ts
// 用户状态管理 Store
// 该文件演示了如何通过 Composition API 共享逻辑
// 设计意图:通过使用共享的 useApi composable,减少代码重复,提高可维护性
import { defineStore } from 'pinia'
import { useApi } from '@/composables/useApi'
import { userApi } from '@/services/api/user' // 假设已导入
/**
* 用户状态管理 Store
* @returns 用户状态管理 Store 实例
*/
export const useUserStore = defineStore('user', () => {
// 使用共享的 API 逻辑
// 优点:代码复用,减少重复逻辑,Store 之间耦合度低
const { loading, error, request } = useApi()
/**
* 用户登录
* 使用共享的 request 方法发起登录请求
* 演示了如何使用共享的 composable 逻辑
* @param email 邮箱
* @param password 密码
* @returns 登录响应,包含用户信息和令牌
* @throws 登录失败时抛出错误
*/
async function login(email: string, password: string) {
return request(() => userApi.login({ email, password }))
}
return {
loading, // 加载状态
error, // 错误信息
login // 登录方法
}
})
3.3 状态持久化策略
对于需要持久化的状态,如用户认证信息、用户偏好设置等,可以使用以下策略:
1. 使用 localStorage/sessionStorage
typescript
// stores/user.ts
// 用户状态管理 Store
// 该文件演示了如何通过 localStorage 实现状态持久化
// 优点:实现简单直接,适用于基本的状态持久化需求
// 缺点:需要手动管理存储逻辑,代码冗余
// 设计意图:通过 localStorage 实现状态持久化,保持用户登录状态
import { defineStore } from 'pinia'
import { ref } from 'vue'
/**
* 用户状态管理 Store
* @returns 用户状态管理 Store 实例
*/
export const useUserStore = defineStore('user', () => {
// 从 localStorage 初始化令牌
// 在 Store 初始化时,从 localStorage 中读取之前存储的令牌
const token = ref<string | null>(localStorage.getItem('token'))
/**
* 设置令牌
* 同时更新 localStorage 中的令牌,实现状态持久化
* @param newToken 新令牌,字符串或 null
*/
function setToken(newToken: string | null) {
token.value = newToken // 更新内存中的令牌状态
if (newToken) {
localStorage.setItem('token', newToken) // 存储令牌到 localStorage
} else {
localStorage.removeItem('token') // 清除 localStorage 中的令牌
}
}
return {
token, // 令牌状态
setToken // 设置令牌的方法
}
})
2. 使用 pinia-plugin-persistedstate
typescript
// stores/user.ts
// 用户状态管理 Store
// 该文件演示了如何通过 pinia-plugin-persistedstate 实现状态持久化
// 优点:配置简单,自动管理存储逻辑,代码简洁
// 缺点:需要安装额外的插件
// 设计意图:通过 pinia-plugin-persistedstate 插件实现状态持久化,简化存储逻辑
import { defineStore } from 'pinia'
import { ref } from 'vue'
import type { User } from '@/models/user' // 假设已导入
/**
* 用户状态管理 Store
* @returns 用户状态管理 Store 实例
*/
export const useUserStore = defineStore('user', () => {
const currentUser = ref<User | null>(null) // 当前用户信息,类型为 User 或 null
const token = ref<string | null>(null) // 认证令牌,用于 API 调用的身份验证
/**
* 用户登录
* @param user 用户信息,类型为 User
* @param userToken 认证令牌,字符串
*/
function login(user: User, userToken: string) {
currentUser.value = user // 更新当前用户信息
token.value = userToken // 更新认证令牌
// 由于配置了 persist,状态会自动持久化到 localStorage
}
/**
* 用户登出
*/
function logout() {
currentUser.value = null // 清空当前用户信息
token.value = null // 清空认证令牌
// 由于配置了 persist,状态会自动从 localStorage 中移除
}
return {
currentUser, // 当前用户信息
token, // 认证令牌
login, // 登录方法
logout // 登出方法
}
}, {
persist: {
key: 'user-store', // 存储键名,用于在 localStorage 中标识存储的数据
storage: localStorage, // 存储方式,使用 localStorage 实现持久化
paths: ['currentUser', 'token'] // 需要持久化的状态路径,只持久化 currentUser 和 token
}
})
4. 路由架构
4.1 路由配置组织
一个合理的路由配置能够清晰地定义应用的导航结构,便于开发者理解和维护。
路由配置文件结构
typescript
// router/index.ts
// 路由主配置文件
// 该文件负责创建路由实例并应用全局守卫
// 设计意图:配置和初始化 Vue Router,应用全局守卫
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { routes } from './routes'
import { authGuard } from './guards'
/**
* 创建路由实例
* 配置路由的历史模式和路由表
* @returns 路由实例
*/
const router = createRouter({
history: createWebHistory(), // 使用 HTML5 History API,移除 URL 中的 # 符号
routes // 路由配置,从 ./routes 文件导入
})
/**
* 全局前置守卫
* 应用认证守卫,处理所有路由的认证和权限检查
*/
router.beforeEach(authGuard)
export default router
// router/routes.ts
// 路由定义文件
// 该文件负责定义应用的所有路由
// 设计意图:集中管理应用的路由配置,包括路由路径、组件映射、元信息等
import { RouteRecordRaw } from 'vue-router'
import DefaultLayout from '@/layouts/DefaultLayout.vue'
import AuthLayout from '@/layouts/AuthLayout.vue'
/**
* 路由元信息接口
* 定义路由的额外信息,如认证要求、页面标题、角色权限等
*/
export interface RouteMeta {
/**
* 是否需要认证
* @default true
*/
requiresAuth?: boolean
/**
* 页面标题
*/
title?: string
/**
* 角色权限
* 只有具有指定角色的用户才能访问该路由
*/
roles?: string[]
/**
* 是否在侧边栏显示
*/
sidebar?: boolean
/**
* 侧边栏图标
* 用于侧边栏导航的图标名称
*/
icon?: string
}
/**
* 扩展的路由记录类型
* 集成自定义的元信息接口,提供更好的类型支持
*/
export type AppRouteRecordRaw = RouteRecordRaw & {
meta?: RouteMeta
children?: AppRouteRecordRaw[]
}
/**
* 路由配置数组
* 定义应用的所有路由,包括布局、页面组件、元信息等
*/
export const routes: AppRouteRecordRaw[] = [
{
path: '/',
component: DefaultLayout, // 使用默认布局(包含侧边栏和头部)
meta: {
requiresAuth: true // 需要认证
},
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'), // 懒加载仪表盘页面
meta: {
title: '仪表盘',
sidebar: true, // 在侧边栏显示
icon: 'dashboard' // 侧边栏图标
}
},
{
path: 'products',
name: 'Products',
component: () => import('@/views/products/index.vue'), // 懒加载产品管理页面
meta: {
title: '产品管理',
sidebar: true, // 在侧边栏显示
icon: 'shopping-cart' // 侧边栏图标
}
},
{
path: 'users',
name: 'Users',
component: () => import('@/views/users/index.vue'), // 懒加载用户管理页面
meta: {
title: '用户管理',
sidebar: true, // 在侧边栏显示
icon: 'users', // 侧边栏图标
roles: ['admin'] // 只有 admin 角色可以访问
}
}
]
},
{
path: '/auth',
component: AuthLayout, // 使用认证布局(简洁布局,无侧边栏)
meta: {
requiresAuth: false // 不需要认证
},
children: [
{
path: 'login',
name: 'Login',
component: () => import('@/views/auth/login.vue'), // 懒加载登录页面
meta: {
title: '登录'
}
},
{
path: 'register',
name: 'Register',
component: () => import('@/views/auth/register.vue'), // 懒加载注册页面
meta: {
title: '注册'
}
}
]
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue'), // 懒加载 404 页面
meta: {
title: '页面不存在'
}
}
]
// router/guards.ts
// 路由守卫文件
// 该文件负责定义路由守卫逻辑
// 设计意图:实现路由的认证和权限控制,保护需要认证的路由
import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
/**
* 认证守卫
* 处理路由的认证和权限检查
* 在每次路由导航前执行,确保用户有足够的权限访问目标路由
* @param to 目标路由对象
* @param from 来源路由对象
* @param next 导航函数,决定导航是否继续
*/
export function authGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
const userStore = useUserStore() // 获取用户状态管理 Store
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - My App`
}
// 权限检查
const requiresAuth = to.meta.requiresAuth !== false // 默认需要认证
const isAuthenticated = userStore.isAuthenticated // 用户是否已认证
const roles = to.meta.roles || [] // 路由所需的角色权限
if (requiresAuth && !isAuthenticated) {
// 未登录,重定向到登录页
next({ name: 'Login' })
} else if (roles.length > 0 && !roles.some(role => userStore.hasRole(role))) {
// 无权限,重定向到 403 页面
next({ name: 'Forbidden' })
} else {
// 已登录或不需要认证,继续导航
next()
}
}
4.2 路由模块化
对于大型应用,可以将路由配置按照模块进行拆分,提高代码的可维护性。
typescript
// router/modules/auth.ts
// 认证模块路由配置
// 设计意图:将认证相关的路由配置拆分到独立的模块,提高代码可维护性
import { AppRouteRecordRaw } from '../types'
/**
* 认证模块路由配置
* 包含登录、注册等认证相关页面的路由
*/
export const authRoutes: AppRouteRecordRaw[] = [
{
path: '/auth',
component: () => import('@/layouts/AuthLayout.vue'), // 懒加载认证布局
meta: {
requiresAuth: false // 认证页面不需要认证
},
children: [
{
path: 'login',
name: 'Login',
component: () => import('@/views/auth/login.vue'), // 懒加载登录页面
meta: {
title: '登录'
}
},
{
path: 'register',
name: 'Register',
component: () => import('@/views/auth/register.vue'), // 懒加载注册页面
meta: {
title: '注册'
}
}
]
}
]
// router/modules/dashboard.ts
// 仪表盘模块路由配置
// 设计意图:将仪表盘相关的路由配置拆分到独立的模块,提高代码可维护性
import { AppRouteRecordRaw } from '../types'
/**
* 仪表盘模块路由配置
* 包含仪表盘页面的路由
*/
export const dashboardRoutes: AppRouteRecordRaw[] = [
{
path: '/dashboard',
component: () => import('@/layouts/DefaultLayout.vue'), // 懒加载默认布局
meta: {
requiresAuth: true // 需要认证
},
children: [
{
path: '',
name: 'Dashboard',
component: () => import('@/views/dashboard/index.vue'), // 懒加载仪表盘页面
meta: {
title: '仪表盘',
sidebar: true, // 在侧边栏显示
icon: 'dashboard' // 侧边栏图标
}
}
]
}
]
// router/routes.ts
// 路由配置主文件
// 设计意图:组合所有路由模块,形成完整的应用路由配置
import { AppRouteRecordRaw } from './types'
import { authRoutes } from './modules/auth' // 认证模块路由
import { dashboardRoutes } from './modules/dashboard' // 仪表盘模块路由
import { productRoutes } from './modules/product' // 产品模块路由
import { userRoutes } from './modules/user' // 用户模块路由
/**
* 路由配置数组
* 组合所有路由模块,形成完整的应用路由配置
*/
export const routes: AppRouteRecordRaw[] = [
...authRoutes, // 认证模块路由
...dashboardRoutes, // 仪表盘模块路由
...productRoutes, // 产品模块路由
...userRoutes, // 用户模块路由
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/error/404.vue'), // 懒加载 404 页面
meta: {
title: '页面不存在'
}
}
]
4.3 路由守卫策略
路由守卫是控制路由访问权限的重要手段,合理使用路由守卫能够提高应用的安全性和用户体验。
全局守卫
全局守卫适用于所有路由,可以用于处理认证、权限检查、页面标题设置等通用逻辑。
typescript
// router/guards.ts
// 路由守卫文件
// 设计意图:实现路由的认证和权限控制,保护需要认证的路由
import { NavigationGuardNext, RouteLocationNormalized } from 'vue-router'
import { useUserStore } from '@/stores/user'
/**
* 认证守卫
* 全局前置守卫,处理所有路由的认证和权限检查
* @param to 目标路由对象
* @param from 来源路由对象
* @param next 导航函数,决定导航是否继续
*/
export function authGuard(to: RouteLocationNormalized, from: RouteLocationNormalized, next: NavigationGuardNext) {
const userStore = useUserStore() // 获取用户状态管理 Store
// 设置页面标题
if (to.meta.title) {
document.title = `${to.meta.title} - My App`
}
// 权限检查
const requiresAuth = to.meta.requiresAuth !== false // 默认需要认证
const isAuthenticated = userStore.isAuthenticated // 用户是否已认证
const roles = to.meta.roles || [] // 路由所需的角色权限
if (requiresAuth && !isAuthenticated) {
// 未登录,重定向到登录页
next({ name: 'Login' })
} else if (roles.length > 0 && !roles.some(role => userStore.hasRole(role))) {
// 无权限,重定向到 403 页面
next({ name: 'Forbidden' })
} else {
// 已登录或不需要认证,继续导航
next()
}
}
/**
* 全局后置守卫
* 在路由导航完成后执行,用于处理页面加载完成后的逻辑
* @param to 目标路由对象
* @param from 来源路由对象
*/
export function globalAfterGuard(to: RouteLocationNormalized, from: RouteLocationNormalized) {
// 页面加载完成后的逻辑,如埋点统计、页面性能监控等
console.log(`Navigated from ${from.path} to ${to.path}`)
}
路由独享守卫
路由独享守卫只适用于特定路由,可以用于处理该路由的特殊逻辑。
typescript
// router/routes.ts
// 路由定义文件
// 设计意图:演示如何使用路由独享守卫处理特定路由的特殊逻辑
import { AppRouteRecordRaw } from './types'
/**
* 路由配置数组
*/
export const routes: AppRouteRecordRaw[] = [
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/admin/index.vue'), // 懒加载管理员页面
meta: {
requiresAuth: true, // 需要认证
roles: ['admin'] // 只有 admin 角色可以访问
},
beforeEnter: (to, from, next) => {
// 路由独享守卫逻辑
// 只适用于当前路由,用于处理该路由的特殊逻辑
console.log('Entering admin route')
// 可以在这里添加额外的权限检查或其他逻辑
next() // 继续导航
}
}
]
组件内守卫
组件内守卫适用于组件级别,可以用于处理组件的进入、离开等逻辑。
vue
<script setup lang="ts">
// 组件内守卫示例
// 设计意图:演示如何在组件级别使用路由守卫,处理组件的进入、离开等逻辑
import { onBeforeRouteEnter, onBeforeRouteLeave, onBeforeRouteUpdate } from 'vue-router'
/**
* 路由进入组件前
* 在路由进入组件之前执行,此时组件实例还未创建
* @param to 目标路由对象
* @param from 来源路由对象
* @param next 导航函数,决定导航是否继续
*/
onBeforeRouteEnter((to, from, next) => {
console.log('Before route enter')
// 可以在这里进行数据预加载、权限检查等
// 例如:获取组件需要的数据,避免组件渲染时出现空白
next() // 继续导航
})
/**
* 路由离开组件前
* 在路由离开组件之前执行,此时组件实例仍然存在
* @param to 目标路由对象
* @param from 来源路由对象
* @param next 导航函数,决定导航是否继续
*/
onBeforeRouteLeave((to, from, next) => {
console.log('Before route leave')
// 可以在这里进行表单验证、确认提示等
// 例如:检查用户是否有未保存的修改,提示用户确认离开
next() // 继续导航
})
/**
* 路由更新但组件被复用时
* 在路由更新但组件被复用时执行,例如从 /user/1 导航到 /user/2
* @param to 目标路由对象
* @param from 来源路由对象
* @param next 导航函数,决定导航是否继续
*/
onBeforeRouteUpdate((to, from, next) => {
console.log('Before route update')
// 可以在这里更新组件数据、重新获取数据等
// 例如:根据新的路由参数重新获取用户信息
next() // 继续导航
})
</script>
5. API 层设计
5.1 API 服务组织
一个合理的 API 层设计能够清晰地管理与后端的通信,提高代码的可维护性和可测试性。
API 服务文件结构
typescript
// services/api/index.ts
// API 客户端配置文件
// 设计意图:创建和配置 axios 实例,设置请求/响应拦截器,统一处理 API 调用
import axios from 'axios'
import { useUserStore } from '@/stores/user'
/**
* 创建 axios 实例
* 配置基础 URL、超时时间、默认请求头等
*/
const apiClient = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api', // API 基础 URL,从环境变量获取或使用默认值
timeout: 10000, // 请求超时时间,10秒
headers: {
'Content-Type': 'application/json' // 默认请求头,使用 JSON 格式
}
})
/**
* 请求拦截器
* 在发送请求之前执行,用于添加认证令牌、请求日志等
*/
apiClient.interceptors.request.use(
(config) => {
const userStore = useUserStore() // 获取用户状态管理 Store
const token = userStore.token // 获取认证令牌
if (token) {
// 如果有令牌,添加到请求头的 Authorization 字段
config.headers.Authorization = `Bearer ${token}`
}
return config // 返回修改后的配置
},
(error) => {
// 请求错误处理
return Promise.reject(error)
}
)
/**
* 响应拦截器
* 在收到响应之后执行,用于统一处理响应数据、错误处理等
*/
apiClient.interceptors.response.use(
(response) => {
// 只返回响应数据,忽略其他响应信息
return response.data
},
(error) => {
// 错误处理
if (error.response?.status === 401) {
// 401 未授权错误,清除用户登录状态
const userStore = useUserStore()
userStore.logout()
}
return Promise.reject(error) // 重新抛出错误,以便调用方处理
}
)
export default apiClient
// services/api/user.ts
// 用户 API 服务
// 设计意图:封装用户相关的 API 调用,提供类型安全的 API 服务
import apiClient from './index'
import { User, LoginRequest, RegisterRequest } from '@/models/user'
/**
* 用户 API 服务
* 封装用户相关的 API 调用,包括登录、注册、获取用户信息、更新用户信息等
*/
export const userApi = {
/**
* 用户登录
* @param data 登录数据,包含邮箱和密码
* @returns 登录响应,包含用户信息和认证令牌
*/
login: (data: LoginRequest) => {
return apiClient.post<{ user: User; token: string }>('/auth/login', data)
},
/**
* 用户注册
* @param data 注册数据,包含用户基本信息
* @returns 注册响应,包含用户信息和认证令牌
*/
register: (data: RegisterRequest) => {
return apiClient.post<{ user: User; token: string }>('/auth/register', data)
},
/**
* 获取当前用户信息
* @returns 当前用户信息
*/
getCurrentUser: () => {
return apiClient.get<User>('/users/me')
},
/**
* 更新用户信息
* @param data 用户信息,部分更新
* @returns 更新后的用户信息
*/
updateUser: (data: Partial<User>) => {
return apiClient.put<User>('/users/me', data)
}
}
// services/api/product.ts
// 产品 API 服务
// 设计意图:封装产品相关的 API 调用,提供类型安全的 API 服务
import apiClient from './index'
import { Product, CreateProductRequest, UpdateProductRequest } from '@/models/product'
/**
* 产品 API 服务
* 封装产品相关的 API 调用,包括获取产品列表、获取产品详情、创建产品、更新产品、删除产品等
*/
export const productApi = {
/**
* 获取产品列表
* @param params 查询参数,包含分页和筛选条件
* @returns 产品列表
*/
getProducts: (params?: { page?: number; limit?: number; category?: string }) => {
return apiClient.get<Product[]>('/products', { params })
},
/**
* 获取产品详情
* @param id 产品 ID
* @returns 产品详情
*/
getProduct: (id: number) => {
return apiClient.get<Product>(`/products/${id}`)
},
/**
* 创建产品
* @param data 产品数据,包含产品基本信息
* @returns 创建的产品
*/
createProduct: (data: CreateProductRequest) => {
return apiClient.post<Product>('/products', data)
},
/**
* 更新产品
* @param id 产品 ID
* @param data 产品数据,部分更新
* @returns 更新后的产品
*/
updateProduct: (id: number, data: UpdateProductRequest) => {
return apiClient.put<Product>(`/products/${id}`, data)
},
/**
* 删除产品
* @param id 产品 ID
* @returns 删除结果
*/
deleteProduct: (id: number) => {
return apiClient.delete(`/products/${id}`)
}
}
5.2 API 错误处理策略
合理的错误处理策略能够提高应用的稳定性和用户体验,以下是几种常见的错误处理方式:
全局错误处理
通过 axios 响应拦截器统一处理 API 错误。
typescript
// services/api/index.ts
// API 客户端配置文件
// 设计意图:通过响应拦截器实现全局错误处理,统一处理不同类型的 API 错误
import axios from 'axios'
import { useUserStore } from '@/stores/user'
import { ElMessage } from 'element-plus'
/**
* 响应拦截器
* 统一处理 API 错误,根据错误状态码显示不同的错误提示
*/
apiClient.interceptors.response.use(
(response) => {
return response.data // 只返回响应数据
},
(error) => {
// 处理网络错误(无响应)
if (!error.response) {
ElMessage.error('网络连接失败,请检查网络设置')
return Promise.reject(error)
}
// 处理 401 错误(未授权)
if (error.response.status === 401) {
const userStore = useUserStore()
userStore.logout() // 清除用户登录状态
ElMessage.error('登录已过期,请重新登录')
return Promise.reject(error)
}
// 处理 403 错误(禁止访问)
if (error.response.status === 403) {
ElMessage.error('没有权限访问该资源')
return Promise.reject(error)
}
// 处理 404 错误(资源不存在)
if (error.response.status === 404) {
ElMessage.error('请求的资源不存在')
return Promise.reject(error)
}
// 处理 500 错误(服务器错误)
if (error.response.status >= 500) {
ElMessage.error('服务器错误,请稍后重试')
return Promise.reject(error)
}
// 处理其他错误
const errorMessage = error.response.data?.message || '请求失败,请稍后重试'
ElMessage.error(errorMessage)
return Promise.reject(error)
}
)
局部错误处理
在具体的 API 调用处进行错误处理,适用于需要特殊处理的场景。
typescript
// composables/useAuth.ts
// 认证相关逻辑
// 设计意图:在具体的 API 调用处进行错误处理,适用于需要特殊处理的场景
import { ref } from 'vue'
import { userApi } from '@/services/api/user'
import { LoginRequest, RegisterRequest } from '@/models/user'
/**
* 认证相关逻辑
* 封装登录、注册等认证操作,并在具体的 API 调用处进行错误处理
*/
export function useAuth() {
const loading = ref(false) // 加载状态
const error = ref<string | null>(null) // 错误信息
/**
* 用户登录
* 在具体的 API 调用处进行错误处理,提供更具体的错误提示
* @param email 邮箱
* @param password 密码
* @returns 登录响应
* @throws 登录失败时抛出错误
*/
async function login(email: string, password: string) {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.login({ email, password }) // 调用登录 API
return response // 返回登录响应
} catch (err: any) {
// 自定义错误信息,提供更具体的错误提示
error.value = err.response?.data?.message || '登录失败,请检查邮箱和密码'
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
/**
* 用户注册
* 在具体的 API 调用处进行错误处理,提供更具体的错误提示
* @param userData 注册数据
* @returns 注册响应
* @throws 注册失败时抛出错误
*/
async function register(userData: RegisterRequest) {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.register(userData) // 调用注册 API
return response // 返回注册响应
} catch (err: any) {
// 自定义错误信息,提供更具体的错误提示
error.value = err.response?.data?.message || '注册失败,请稍后重试'
throw err // 重新抛出错误,以便调用方处理
} finally {
loading.value = false // 结束加载
}
}
return {
loading, // 加载状态
error, // 错误信息
login, // 登录方法
register // 注册方法
}
}
5.3 API 数据缓存策略
合理的 API 数据缓存策略能够减少重复请求,提高应用的性能和用户体验。
内存缓存
使用内存缓存存储临时数据,适用于单次会话中的数据。
typescript
// services/api/product.ts
// 产品 API 服务
// 设计意图:通过内存缓存减少重复请求,提高应用性能和用户体验
import apiClient from './index'
import { Product } from '@/models/product'
// 产品列表缓存
// 使用内存变量存储产品列表数据和缓存时间戳
let productsCache: { data: Product[]; timestamp: number } | null = null
const CACHE_DURATION = 5 * 60 * 1000 // 缓存持续时间,5 分钟
/**
* 产品 API 服务
*/
export const productApi = {
/**
* 获取产品列表
* 使用内存缓存减少重复请求,提高应用性能
* @param params 查询参数,包含分页和筛选条件
* @returns 产品列表
*/
getProducts: async (params?: { page?: number; limit?: number; category?: string }) => {
// 检查缓存是否有效
// 如果缓存存在且未过期,直接返回缓存数据
if (productsCache && Date.now() - productsCache.timestamp < CACHE_DURATION) {
return productsCache.data
}
// 缓存无效或不存在,发起 API 请求
const data = await apiClient.get<Product[]>('/products', { params })
// 更新缓存
// 存储响应数据和当前时间戳
productsCache = {
data,
timestamp: Date.now()
}
return data
},
/**
* 清除产品列表缓存
* 在产品数据发生变化时调用,确保下次获取最新数据
*/
clearProductsCache: () => {
productsCache = null
}
}
本地存储缓存
使用 localStorage 或 sessionStorage 存储持久化数据,适用于需要跨会话保持的数据。
typescript
// services/api/user.ts
// 用户 API 服务
// 设计意图:通过本地存储缓存实现数据持久化,适用于需要跨会话保持的数据
import apiClient from './index'
import { User } from '@/models/user'
const USER_CACHE_KEY = 'user_cache' // 本地存储的缓存键名
const CACHE_DURATION = 24 * 60 * 60 * 1000 // 缓存持续时间,24 小时
/**
* 用户 API 服务
*/
export const userApi = {
/**
* 获取当前用户信息
* 使用本地存储缓存实现数据持久化,减少重复请求
* @returns 用户信息
*/
getCurrentUser: async () => {
// 检查缓存
const cachedUser = localStorage.getItem(USER_CACHE_KEY)
if (cachedUser) {
const { data, timestamp } = JSON.parse(cachedUser)
// 如果缓存存在且未过期,直接返回缓存数据
if (Date.now() - timestamp < CACHE_DURATION) {
return data
}
}
// 缓存无效或不存在,发起 API 请求
const data = await apiClient.get<User>('/users/me')
// 更新缓存
// 将响应数据和当前时间戳存储到本地存储
localStorage.setItem(USER_CACHE_KEY, JSON.stringify({
data,
timestamp: Date.now()
}))
return data
},
/**
* 清除用户缓存
* 在用户信息发生变化时调用,确保下次获取最新数据
*/
clearUserCache: () => {
localStorage.removeItem(USER_CACHE_KEY)
}
}
6. 工具类和公共组件
6.1 工具函数组织
工具函数是项目中常用的辅助函数,合理组织工具函数能够提高代码的复用性和可维护性。
工具函数目录结构
src/utils/
├── format.ts # 格式化工具
├── validation.ts # 验证工具
├── storage.ts # 存储工具
├── http.ts # HTTP 工具
├── date.ts # 日期工具
├── number.ts # 数字工具
├── string.ts # 字符串工具
├── array.ts # 数组工具
└── object.ts # 对象工具
工具函数示例
typescript
// utils/format.ts
// 格式化工具函数
// 设计意图:提供常用的格式化功能,如金额格式化、日期格式化等
/**
* 格式化金额
* 将数字金额格式化为带有货币符号和千分位的字符串
* @param amount 金额,数字类型
* @param currency 货币符号,默认为 ¥
* @returns 格式化后的金额字符串,如 "¥1,234.56"
*/
export function formatCurrency(amount: number, currency: string = '¥'): string {
// 使用 toFixed(2) 保留两位小数,然后使用正则表达式添加千分位
return `${currency}${amount.toFixed(2).replace(/\B(?=(\d{3})+(?!\d))/g, ',')}`
}
/**
* 格式化日期
* 将日期对象、字符串或时间戳格式化为指定格式的日期字符串
* @param date 日期,可以是 Date 对象、日期字符串或时间戳
* @param format 格式化模板,默认为 'YYYY-MM-DD'
* @returns 格式化后的日期字符串,如 "2023-12-25"
*/
export function formatDate(date: Date | string | number, format: string = 'YYYY-MM-DD'): string {
const d = new Date(date) // 创建 Date 对象
const year = d.getFullYear() // 获取年份
const month = String(d.getMonth() + 1).padStart(2, '0') // 获取月份(0-11,所以加 1),并补零
const day = String(d.getDate()).padStart(2, '0') // 获取日期,并补零
const hours = String(d.getHours()).padStart(2, '0') // 获取小时,并补零
const minutes = String(d.getMinutes()).padStart(2, '0') // 获取分钟,并补零
const seconds = String(d.getSeconds()).padStart(2, '0') // 获取秒数,并补零
// 替换格式化模板中的占位符
return format
.replace('YYYY', String(year))
.replace('MM', month)
.replace('DD', day)
.replace('HH', hours)
.replace('mm', minutes)
.replace('ss', seconds)
}
// utils/validation.ts
// 验证工具函数
// 设计意图:提供常用的验证功能,如邮箱验证、密码强度验证等
/**
* 验证邮箱
* 检查邮箱地址是否符合标准格式
* @param email 邮箱地址,字符串类型
* @returns 是否为有效邮箱,布尔值
*/
export function isValidEmail(email: string): boolean {
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ // 简单的邮箱格式正则表达式
return emailRegex.test(email) // 测试邮箱地址是否匹配正则表达式
}
/**
* 验证密码强度
* 根据密码长度、包含的字符类型等计算密码强度等级
* @param password 密码,字符串类型
* @returns 密码强度等级 (0-4),数字越大表示密码强度越高
*/
export function getPasswordStrength(password: string): number {
let strength = 0 // 初始强度为 0
if (password.length >= 8) strength++ // 长度至少 8 位
if (/[A-Z]/.test(password)) strength++ // 包含大写字母
if (/[a-z]/.test(password)) strength++ // 包含小写字母
if (/[0-9]/.test(password)) strength++ // 包含数字
if (/[^A-Za-z0-9]/.test(password)) strength++ // 包含特殊字符
return strength // 返回密码强度等级
}
// utils/storage.ts
// 存储工具函数
// 设计意图:封装本地存储操作,提供类型安全的存储工具函数
/**
* 设置本地存储
* 将值存储到 localStorage 中,自动进行 JSON 序列化
* @param key 存储键,字符串类型
* @param value 存储值,可以是任何可 JSON 序列化的类型
*/
export function setLocalStorage(key: string, value: any): void {
try {
// 将值转换为 JSON 字符串并存储
localStorage.setItem(key, JSON.stringify(value))
} catch (error) {
// 捕获并记录错误,避免存储失败导致应用崩溃
console.error('Error setting localStorage:', error)
}
}
/**
* 获取本地存储
* 从 localStorage 中获取值,自动进行 JSON 反序列化
* @param key 存储键,字符串类型
* @param defaultValue 默认值,当存储不存在或解析失败时返回
* @returns 存储值或默认值,类型为 T
*/
export function getLocalStorage<T>(key: string, defaultValue: T): T {
try {
const value = localStorage.getItem(key) // 获取存储的 JSON 字符串
return value ? JSON.parse(value) : defaultValue // 解析 JSON 字符串,失败则返回默认值
} catch (error) {
// 捕获并记录错误,返回默认值
console.error('Error getting localStorage:', error)
return defaultValue
}
}
/**
* 删除本地存储
* 从 localStorage 中删除指定的存储项
* @param key 存储键,字符串类型
*/
export function removeLocalStorage(key: string): void {
try {
localStorage.removeItem(key) // 删除存储项
} catch (error) {
// 捕获并记录错误
console.error('Error removing localStorage:', error)
}
}
6.2 公共组件组织
公共组件是项目中可复用的 UI 组件,合理组织公共组件能够提高开发效率和 UI 一致性。
公共组件目录结构
src/components/
├── common/ # 通用基础组件
│ ├── Button.vue # 按钮组件
│ ├── Input.vue # 输入框组件
│ ├── Dialog.vue # 对话框组件
│ └── Loading.vue # 加载组件
├── layout/ # 布局相关组件
│ ├── Header.vue # 头部组件
│ ├── Sidebar.vue # 侧边栏组件
│ └── Footer.vue # 底部组件
└── business/ # 业务相关组件
├── UserCard.vue # 用户卡片组件
├── ProductList.vue # 产品列表组件
└── OrderForm.vue # 订单表单组件
公共组件设计原则
- 单一职责:每个组件只负责一个功能,便于理解和维护
- 可配置性:通过 props 提供足够的配置选项,提高组件的灵活性
- 可扩展性:设计合理的组件结构,便于后续功能的扩展
- 类型安全:充分利用 TypeScript 的类型系统,为组件定义明确的类型
- 文档完备:为组件提供详细的文档和使用示例
公共组件示例
vue
<!-- components/common/Button.vue -->
<!-- 通用按钮组件 -->
<!-- 设计意图:提供一个可配置的通用按钮组件,支持多种类型、尺寸和状态 -->
<template>
<button
:class="[
'btn',
`btn-${variant}`, // 按钮类型样式
`btn-${size}`, // 按钮尺寸样式
{ 'btn-block': block }, // 块级按钮样式
{ 'btn-disabled': disabled } // 禁用状态样式
]"
:disabled="disabled" // 禁用状态
@click="$emit('click', $event)" // 点击事件
>
<slot></slot> <!-- 按钮内容插槽 -->
</button>
</template>
<script setup lang="ts">
// 使用 Composition API 语法
import { defineProps, defineEmits } from 'vue'
/**
* 按钮类型
* 支持多种预设的按钮样式
*/
export type ButtonVariant = 'primary' | 'secondary' | 'success' | 'danger' | 'warning' | 'info'
/**
* 按钮尺寸
* 支持多种预设的按钮尺寸
*/
export type ButtonSize = 'sm' | 'md' | 'lg'
/**
* 按钮组件属性
* 使用 TypeScript 类型定义,提供更好的类型提示
*/
const props = defineProps<{
/**
* 按钮类型
* 决定按钮的颜色样式
* @default 'primary'
*/
variant?: ButtonVariant
/**
* 按钮尺寸
* 决定按钮的大小
* @default 'md'
*/
size?: ButtonSize
/**
* 是否为块级按钮
* 块级按钮会占满父容器的宽度
* @default false
*/
block?: boolean
/**
* 是否禁用
* 禁用状态下按钮不可点击
* @default false
*/
disabled?: boolean
}>()
/**
* 按钮组件事件
* 使用 TypeScript 类型定义,提供更好的类型提示
*/
const emit = defineEmits<{
/**
* 点击事件
* 当用户点击按钮时触发
* @param event 点击事件对象
*/
(e: 'click', event: MouseEvent): void
}>()
</script>
<style scoped>
/* 按钮基础样式 */
.btn {
display: inline-block;
padding: 0.5rem 1rem;
border: none;
border-radius: 0.25rem;
font-weight: 500;
cursor: pointer;
transition: all 0.2s ease;
}
/* 按钮类型样式 */
.btn-primary {
background-color: #3b82f6;
color: white;
}
.btn-secondary {
background-color: #6b7280;
color: white;
}
.btn-success {
background-color: #10b981;
color: white;
}
.btn-danger {
background-color: #ef4444;
color: white;
}
.btn-warning {
background-color: #f59e0b;
color: white;
}
.btn-info {
background-color: #3b82f6;
color: white;
}
/* 按钮尺寸样式 */
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
}
.btn-md {
padding: 0.5rem 1rem;
font-size: 1rem;
}
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.125rem;
}
/* 块级按钮样式 */
.btn-block {
display: block;
width: 100%;
}
/* 禁用状态样式 */
.btn-disabled {
opacity: 0.6;
cursor: not-allowed;
}
</style>
6.3 组合式 API 逻辑组织
组合式 API 是 Vue 3 的重要特性,合理组织组合式 API 逻辑能够提高代码的复用性和可维护性。
组合式 API 目录结构
src/composables/
├── useAuth.ts # 认证相关逻辑
├── useApi.ts # API 调用逻辑
├── useLocalStorage.ts # 本地存储逻辑
├── useValidation.ts # 验证相关逻辑
├── useDebounce.ts # 防抖逻辑
└── useThrottle.ts # 节流逻辑
组合式 API 示例
typescript
// composables/useAuth.ts
// 认证相关逻辑
// 设计意图:封装认证相关的逻辑,供多个组件复用,实现逻辑的模块化和可维护性
import { ref, computed } from 'vue'
import { userApi } from '@/services/api/user'
import { useUserStore } from '@/stores/user'
import { LoginRequest, RegisterRequest } from '@/models/user'
/**
* 认证相关逻辑
* 封装登录、注册、登出等认证操作,提供统一的认证状态管理
* 使用 Composition API 风格,便于在组件中使用
* @returns 认证相关的状态和方法
*/
export function useAuth() {
const userStore = useUserStore() // 获取用户状态管理 Store
const loading = ref(false) // 加载状态
const error = ref<string | null>(null) // 错误信息
/**
* 是否已认证
* 从用户 Store 中获取认证状态
*/
const isAuthenticated = computed(() => userStore.isAuthenticated)
/**
* 当前用户
* 从用户 Store 中获取当前用户信息
*/
const currentUser = computed(() => userStore.currentUser)
/**
* 用户登录
* 调用登录 API,更新用户状态
* @param email 邮箱
* @param password 密码
* @returns 登录响应
* @throws 登录失败时抛出错误
*/
async function login(email: string, password: string) {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.login({ email, password }) // 调用登录 API
userStore.login(response.user, response.token) // 更新用户状态
return response // 返回登录响应
} catch (err: any) {
// 自定义错误信息
error.value = err.response?.data?.message || '登录失败,请检查邮箱和密码'
throw err // 重新抛出错误
} finally {
loading.value = false // 结束加载
}
}
/**
* 用户注册
* 调用注册 API,更新用户状态
* @param userData 用户注册数据
* @returns 注册响应
* @throws 注册失败时抛出错误
*/
async function register(userData: RegisterRequest) {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const response = await userApi.register(userData) // 调用注册 API
userStore.login(response.user, response.token) // 更新用户状态
return response // 返回注册响应
} catch (err: any) {
// 自定义错误信息
error.value = err.response?.data?.message || '注册失败,请稍后重试'
throw err // 重新抛出错误
} finally {
loading.value = false // 结束加载
}
}
/**
* 用户登出
* 调用登出 API,清除用户状态
* @throws 登出失败时抛出错误
*/
async function logout() {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
await userApi.logout() // 调用登出 API
userStore.logout() // 清除用户状态
} catch (err: any) {
// 自定义错误信息
error.value = err.response?.data?.message || '登出失败,请稍后重试'
throw err // 重新抛出错误
} finally {
loading.value = false // 结束加载
}
}
/**
* 获取当前用户信息
* 调用获取用户信息 API,更新用户状态
* @returns 用户信息
* @throws 获取失败时抛出错误
*/
async function fetchCurrentUser() {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
const user = await userApi.getCurrentUser() // 调用获取用户信息 API
userStore.setCurrentUser(user) // 更新用户状态
return user // 返回用户信息
} catch (err: any) {
// 自定义错误信息
error.value = err.response?.data?.message || '获取用户信息失败'
throw err // 重新抛出错误
} finally {
loading.value = false // 结束加载
}
}
return {
loading, // 加载状态
error, // 错误信息
isAuthenticated, // 是否已认证
currentUser, // 当前用户
login, // 登录方法
register, // 注册方法
logout, // 登出方法
fetchCurrentUser // 获取当前用户信息方法
}
}
// composables/useApi.ts
// API 调用逻辑
// 设计意图:封装 API 请求的通用逻辑,供多个组件和 composables 复用,实现错误处理和加载状态的统一管理
import { ref } from 'vue'
/**
* API 调用逻辑
* 封装 API 请求的通用逻辑,包括加载状态管理和错误处理
* 使用 Composition API 风格,便于在组件和其他 composables 中使用
* @returns API 调用相关的状态和方法
*/
export function useApi() {
const loading = ref(false) // 加载状态
const error = ref<string | null>(null) // 错误信息
/**
* 发起 API 请求
* 封装 API 请求的通用逻辑,处理加载状态和错误
* @param apiCall API 调用函数,返回 Promise
* @returns API 响应数据
* @throws API 请求失败时抛出错误
*/
async function request<T>(apiCall: () => Promise<T>): Promise<T> {
loading.value = true // 开始加载
error.value = null // 清空错误信息
try {
return await apiCall() // 执行 API 调用
} catch (err: any) {
// 自定义错误信息
error.value = err.response?.data?.message || '请求失败,请稍后重试'
throw err // 重新抛出错误
} finally {
loading.value = false // 结束加载
}
}
return {
loading, // 加载状态
error, // 错误信息
request // API 请求方法
}
}
7. 企业级项目案例分析
7.1 项目架构演进
初始架构
在项目初期,通常会采用相对简单的架构,以快速实现核心功能。
// 初始架构
// 项目初期的简单架构,以快速实现核心功能为目标
// 设计意图:在项目初期,使用简单的目录结构,快速搭建项目框架
// 特点:
// - 目录结构简单,易于理解和快速上手
// - 功能模块划分明确,便于初期开发
// - 适用于中小型项目或项目初期阶段
// 适用场景:项目初期,功能相对简单,团队规模较小
src/
├── assets/ # 静态资源(图片、样式等)
├── components/ # 公共组件
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理(如 Vuex)
├── services/ # API 服务
├── utils/ # 工具函数
├── App.vue # 根组件
└── main.ts # 应用入口文件
演进架构
随着项目规模的扩大,架构会逐渐演进,添加更多的功能模块和组织结构。
// 演进架构
// 随着项目规模扩大,架构逐渐演进,添加更多功能模块和组织结构
// 设计意图:通过完善的目录结构,提高代码的可维护性和可扩展性,适应项目的不断发展
// 特点:
// - 目录结构更加完善,功能模块划分更加细致
// - 引入了更多的 TypeScript 相关目录,如 types、enums 等
// - 使用 Pinia 替代 Vuex,采用 Composition API 风格
// - 增加了更多的功能模块,如 composables、layouts、models 等
// 适用场景:中大型项目,功能相对复杂,团队规模较大
src/
├── assets/ # 静态资源(图片、样式、图标等)
├── components/ # 公共组件(按功能或类型划分)
├── composables/ # 组合式 API 逻辑(可复用的业务逻辑)
├── constants/ # 常量定义(API 地址、路由名称等)
├── enums/ # 枚举类型(用户角色、订单状态等)
├── layouts/ # 布局组件(默认布局、认证布局等)
├── models/ # 数据模型(用户模型、产品模型等)
├── router/ # 路由配置(路由定义、守卫等)
├── services/ # 服务层(API 服务、工具服务等)
├── stores/ # Pinia 状态管理(按模块划分的状态管理)
├── types/ # TypeScript 类型定义(API 类型、组件类型等)
├── utils/ # 工具函数(格式化、验证、存储等)
├── views/ # 页面组件(按业务功能划分)
├── App.vue # 根组件
├── main.ts # 应用入口文件
└── env.d.ts # 环境变量类型声明
微前端架构
对于超大型应用,可以考虑采用微前端架构,将应用拆分为多个独立的微应用。
// 微前端架构
// 对于超大型应用,采用微前端架构,将应用拆分为多个独立的微应用
// 设计意图:通过微前端架构,实现应用的模块化和独立部署,提高开发效率和团队协作
// 特点:
// - 将应用拆分为多个独立的微应用,每个微应用可以独立开发、部署和维护
// - 主应用(shell)负责微应用的加载和管理
// - 共享资源(packages)供多个微应用复用
// - 提高了应用的可扩展性和可维护性
// 适用场景:超大型应用,多个团队负责不同业务模块,需要独立部署和维护
apps/ # 应用目录,包含所有微应用
├── shell/ # 主应用(shell),负责微应用的加载和管理
├── auth/ # 认证微应用,处理用户登录、注册等认证功能
├── dashboard/ # 仪表盘微应用,展示用户仪表盘和概览数据
└── products/ # 产品微应用,处理产品相关的功能
packages/ # 共享资源目录,包含多个微应用共用的代码
├── components/ # 共享组件,供多个微应用复用
├── utils/ # 共享工具函数,供多个微应用复用
└── types/ # 共享类型定义,供多个微应用复用
7.2 架构优化策略
-
性能优化:
- 代码分割和懒加载
- 资源压缩和缓存
- 减少不必要的重渲染
-
可维护性优化:
- 模块化和组件化
- 代码规范和命名约定
- 文档和注释
-
可扩展性优化:
- 插件化架构
- 配置驱动开发
- 依赖注入
-
安全性优化:
- 认证和授权
- 数据验证和 sanitization
- 防止 XSS 和 CSRF 攻击
7.3 实际项目案例
电商平台
架构特点:
- 模块化设计,按业务功能划分模块
- 微前端架构,将不同业务域拆分为独立的微应用
- 服务端渲染,提高首屏加载速度和 SEO
- 实时数据更新,使用 WebSocket 实现商品价格和库存的实时更新
核心模块:
- 用户认证模块:处理用户登录、注册、密码重置等
- 商品模块:处理商品列表、详情、搜索等
- 购物车模块:处理购物车添加、修改、结算等
- 订单模块:处理订单创建、支付、物流等
- 支付模块:集成多种支付方式
企业管理系统
架构特点:
- 权限管理系统,基于角色的访问控制
- 工作流引擎,支持自定义业务流程
- 数据可视化,提供丰富的报表和图表
- 多语言支持,适应国际化需求
核心模块:
- 用户管理模块:处理用户信息、角色、权限等
- 组织管理模块:处理部门、职位、员工等
- 资产管理模块:处理资产登记、折旧、盘点等
- 财务管理模块:处理预算、报销、审批等
- 项目管理模块:处理项目计划、任务、进度等
8. 总结与实践
8.1 项目架构实践
-
合理的目录结构:
- 按照功能模块组织代码
- 建立清晰的层次结构
- 采用一致的命名规范
-
状态管理策略:
- 使用 Pinia 进行状态管理
- 按模块划分 Store
- 合理使用持久化存储
-
路由架构设计:
- 模块化路由配置
- 合理使用路由守卫
- 支持动态路由和权限控制
-
API 层设计:
- 统一的 API 服务封装
- 合理的错误处理策略
- 有效的数据缓存机制
-
工具类和公共组件:
- 可复用的工具函数
- 可配置的公共组件
- 类型安全的组合式 API
-
代码质量保证:
- 严格的 TypeScript 类型检查
- 统一的代码规范和格式化
- 完善的测试用例
8.2 团队协作实践
-
代码规范:
- 制定统一的代码规范
- 使用 ESLint 和 Prettier 进行代码检查和格式化
- 定期进行代码审查
-
版本控制:
- 采用 Git 工作流
- 合理的分支管理策略
- 规范的提交信息
-
文档管理:
- 项目架构文档
- 组件和 API 文档
- 开发和部署指南
-
自动化工具:
- CI/CD 流程
- 自动化测试
- 代码质量检测
8.3 未来发展趋势
-
微前端架构:
- 将大型应用拆分为多个独立的微应用
- 提高团队协作效率和应用可维护性
-
Serverless 架构:
- 前端和后端都采用 Serverless 架构
- 降低运维成本和提高扩展性
-
AI 辅助开发:
- 使用 AI 工具辅助代码生成和优化
- 提高开发效率和代码质量
-
WebAssembly:
- 部分计算密集型任务使用 WebAssembly
- 提高应用性能
-
边缘计算:
- 将部分计算和存储迁移到边缘节点
- 减少延迟和提高用户体验
9. 附录
9.1 技术栈推荐
| 类别 | 技术 | 版本 | 用途 |
总结
Vue 3 + TypeScript 项目架构设计是一个持续迭代和优化的过程,需要根据项目的具体需求和团队的实际情况进行灵活调整和优化。通过合理的架构设计和实践,我们可以构建出高质量、可维护、可扩展的 Vue 3 + TypeScript 项目,为前端开发团队带来更好的开发体验和更高的生产效率。