随着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
{{ user.username }}
{{ user.email }}
五、组合式函数(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
用户管理
ID
用户名
邮箱
状态
操作
{{ user.id }}
{{ user.username }}
{{ user.email }}
{{ user.status ? '启用' : '禁用' }}
暂无数据
{{ currentPage }} / {{ Math.ceil(total / pageSize) || 1 }}
八、项目配置优化
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

















暂无评论内容