前言
随着前端技术的快速发展,Vue 3作为新一代的前端框架,结合TypeScript的强大类型系统,为构建企业级应用提供了坚实的基础。本文将详细介绍如何从零开始搭建一个完整的Vue 3 + TypeScript企业级项目架构,涵盖项目结构设计、状态管理、路由配置、组件封装、构建优化等核心内容。
项目初始化与基础配置
创建项目结构
首先,我们使用Vue CLI或Vite来创建项目。这里以Vite为例:
npm create vue@latest my-enterprise-app
cd my-enterprise-app
npm install
在创建过程中选择TypeScript支持,这样会自动配置好TypeScript环境。
项目目录结构设计
一个良好的企业级项目架构应该具备清晰的分层结构:
src/
├── assets/ # 静态资源
│ ├── images/
│ ├── styles/
│ └── icons/
├── components/ # 公共组件
│ ├── common/
│ ├── layout/
│ └── ui/
├── composables/ # 可复用逻辑
├── hooks/ # 自定义Hook
├── views/ # 页面组件
├── router/ # 路由配置
├── store/ # 状态管理
├── services/ # API服务
├── utils/ # 工具函数
├── types/ # 类型定义
├── layouts/ # 布局组件
└── App.vue
TypeScript配置优化
在tsconfig.json中进行详细的配置:
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"jsx": "preserve",
"sourceMap": true,
"resolveJsonModule": true,
"esModuleInterop": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"skipLibCheck": true,
"noEmit": true,
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
状态管理架构设计
Pinia状态管理方案
在Vue 3中,Pinia是推荐的状态管理库。首先安装:
npm install pinia
创建Store基础结构
// src/store/index.ts
import { createPinia } from 'pinia'
import { App } from 'vue'
export function setupStore(app: App) {
const pinia = createPinia()
app.use(pinia)
}
export * from './modules/user'
export * from './modules/app'
用户状态模块示例
// src/store/modules/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export interface User {
id: number
name: string
email: string
role: string
}
export const useUserStore = defineStore('user', () => {
const user = ref<User | null>(null)
const isAuthenticated = computed(() => !!user.value)
const setUser = (userData: User) => {
user.value = userData
}
const clearUser = () => {
user.value = null
}
return {
user,
isAuthenticated,
setUser,
clearUser
}
})
应用状态模块
// src/store/modules/app.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useAppStore = defineStore('app', () => {
const loading = ref(false)
const theme = ref<'light' | 'dark'>('light')
const language = ref<'zh' | 'en'>('zh')
const setLoading = (status: boolean) => {
loading.value = status
}
const toggleTheme = () => {
theme.value = theme.value === 'light' ? 'dark' : 'light'
}
return {
loading,
theme,
language,
setLoading,
toggleTheme
}
})
路由系统设计
路由配置基础结构
// src/router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { App } from 'vue'
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/Login.vue'),
meta: { requiresAuth: false }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
export function setupRouter(app: App) {
app.use(router)
}
export default router
路由守卫实现
// src/router/guard.ts
import { router } from './index'
import { useUserStore } from '@/store/modules/user'
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isAuthenticated) {
next('/login')
} else {
next()
}
})
组件化开发实践
公共组件设计原则
// src/components/common/Button.vue
<template>
<button
:class="['btn', `btn--${type}`, { 'btn--disabled': disabled }]"
:disabled="disabled"
@click="handleClick"
>
<slot />
</button>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
interface Props {
type?: 'primary' | 'secondary' | 'danger'
disabled?: boolean
}
const props = withDefaults(defineProps<Props>(), {
type: 'primary',
disabled: false
})
const emit = defineEmits<{
(e: 'click'): void
}>()
const handleClick = () => {
if (!props.disabled) {
emit('click')
}
}
</script>
<style scoped lang="scss">
.btn {
padding: 8px 16px;
border: none;
border-radius: 4px;
cursor: pointer;
transition: all 0.3s ease;
&--primary {
background-color: #007bff;
color: white;
&:hover:not(.btn--disabled) {
background-color: #0056b3;
}
}
&--secondary {
background-color: #6c757d;
color: white;
&:hover:not(.btn--disabled) {
background-color: #545b62;
}
}
&--disabled {
opacity: 0.6;
cursor: not-allowed;
}
}
</style>
布局组件封装
// src/layouts/MainLayout.vue
<template>
<div class="main-layout">
<header class="layout-header">
<slot name="header" />
</header>
<aside class="layout-aside">
<slot name="sidebar" />
</aside>
<main class="layout-main">
<slot />
</main>
</div>
</template>
<script setup lang="ts">
// 布局组件逻辑
</script>
<style scoped lang="scss">
.main-layout {
display: grid;
grid-template-areas:
"header header"
"sidebar main";
grid-template-rows: 60px 1fr;
grid-template-columns: 250px 1fr;
min-height: 100vh;
}
.layout-header {
grid-area: header;
background-color: #f8f9fa;
border-bottom: 1px solid #dee2e6;
}
.layout-aside {
grid-area: sidebar;
background-color: #f8f9fa;
border-right: 1px solid #dee2e6;
}
.layout-main {
grid-area: main;
padding: 20px;
}
</style>
API服务层设计
HTTP客户端封装
// src/services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'
import { useUserStore } from '@/store/modules/user'
class ApiService {
private client: AxiosInstance
constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL,
timeout: 10000
})
this.setupInterceptors()
}
private setupInterceptors() {
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
const userStore = useUserStore()
if (userStore.isAuthenticated && config.headers) {
config.headers.Authorization = `Bearer ${userStore.user?.token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
this.client.interceptors.response.use(
(response) => response.data,
(error) => {
if (error.response?.status === 401) {
// 处理未授权
const userStore = useUserStore()
userStore.clearUser()
window.location.href = '/login'
}
return Promise.reject(error)
}
)
}
public get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.get(url, config)
}
public post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.post(url, data, config)
}
public put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.put(url, data, config)
}
public delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.delete(url, config)
}
}
export const apiService = new ApiService()
业务API服务
// src/services/user.ts
import { apiService } from './api'
import { User } from '@/types/user'
export class UserService {
static async getCurrentUser(): Promise<User> {
return apiService.get('/user/me')
}
static async updateUser(userData: Partial<User>): Promise<User> {
return apiService.put('/user', userData)
}
static async getUserList(page: number, size: number): Promise<{users: User[], total: number}> {
return apiService.get('/users', {
params: { page, size }
})
}
}
// src/types/user.ts
export interface User {
id: number
name: string
email: string
role: string
avatar?: string
createdAt: string
}
工具函数与类型定义
常用工具函数
// src/utils/index.ts
import { Ref } from 'vue'
export const formatDate = (date: Date | string, format: string = 'YYYY-MM-DD'): string => {
const d = new Date(date)
const year = d.getFullYear()
const month = String(d.getMonth() + 1).padStart(2, '0')
const day = String(d.getDate()).padStart(2, '0')
return format
.replace('YYYY', year.toString())
.replace('MM', month)
.replace('DD', day)
}
export const debounce = <T extends (...args: any[]) => any>(
func: T,
wait: number
): ((...args: Parameters<T>) => void) => {
let timeoutId: NodeJS.Timeout | null = null
return function (...args: Parameters<T>) {
if (timeoutId) {
clearTimeout(timeoutId)
}
timeoutId = setTimeout(() => func(...args), wait)
}
}
export const throttle = <T extends (...args: any[]) => any>(
func: T,
limit: number
): ((...args: Parameters<T>) => void) => {
let inThrottle: boolean
return function (...args: Parameters<T>) {
if (!inThrottle) {
func(...args)
inThrottle = true
setTimeout(() => (inThrottle = false), limit)
}
}
}
export const deepClone = <T>(obj: T): T => {
return JSON.parse(JSON.stringify(obj))
}
类型定义文件
// src/types/index.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
timestamp: number
}
export interface PaginationParams {
page: number
size: number
sortBy?: string
sortOrder?: 'asc' | 'desc'
}
export interface PaginationResponse<T> {
items: T[]
total: number
page: number
size: number
}
构建优化与性能调优
Vite配置优化
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import AutoImport from 'unplugin-auto-import/vite'
import Components from 'unplugin-vue-components/vite'
import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
export default defineConfig({
plugins: [
vue(),
AutoImport({
resolvers: [ElementPlusResolver()]
}),
Components({
resolvers: [ElementPlusResolver()]
})
],
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus'],
utils: ['lodash-es']
}
}
}
},
server: {
port: 3000,
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
代码分割与懒加载
// src/router/index.ts (优化版本)
const routes: Array<RouteRecordRaw> = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/Dashboard.vue'),
meta: { requiresAuth: true }
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/Admin.vue'),
meta: { requiresAuth: true, roles: ['admin'] },
children: [
{
path: 'users',
name: 'Users',
component: () => import('@/views/admin/Users.vue')
},
{
path: 'settings',
name: 'Settings',
component: () => import('@/views/admin/Settings.vue')
}
]
}
]
开发环境与测试配置
ESLint和Prettier配置
// .eslintrc.json
{
"extends": [
"@vue/typescript/recommended",
"@vue/prettier"
],
"rules": {
"no-console": "warn",
"no-debugger": "error"
}
}
单元测试配置
// src/__tests__/components/Button.test.ts
import { mount } from '@vue/test-utils'
import Button from '@/components/common/Button.vue'
describe('Button', () => {
it('renders correctly with default props', () => {
const wrapper = mount(Button)
expect(wrapper.classes()).toContain('btn--primary')
})
it('emits click event when clicked', async () => {
const wrapper = mount(Button)
await wrapper.trigger('click')
expect(wrapper.emitted('click')).toHaveLength(1)
})
})
部署与CI/CD流程
构建脚本优化
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"preview": "vite preview",
"test": "vitest",
"lint": "eslint src --ext .ts,.vue --fix",
"type-check": "vue-tsc --noEmit"
}
}
Docker部署配置
# Dockerfile
FROM node:18-alpine
WORKDIR /app
COPY package*.json ./
RUN npm ci --only=production
COPY . .
RUN npm run build
EXPOSE 3000
CMD ["npm", "run", "preview"]
最佳实践总结
代码规范与团队协作
- 组件命名规范:使用PascalCase,如
UserProfile.vue - 状态管理原则:单一职责,避免过度嵌套
- API调用规范:统一错误处理和拦截器配置
- 类型安全:充分利用TypeScript的类型系统
性能优化要点
- 懒加载组件:减少初始包大小
- 代码分割:按路由或功能模块分割代码
- 缓存策略:合理使用computed和watch
- 虚拟滚动:大数据量列表优化
安全性考虑
- API安全:Token验证和权限控制
- 输入验证:表单数据校验
- XSS防护:内容转义处理
- CORS配置:合理的跨域策略
结语
通过本文的详细介绍,我们构建了一个完整的Vue 3 + TypeScript企业级项目架构。这个架构具备良好的扩展性、可维护性和性能表现,能够满足大多数企业级应用的需求。在实际开发中,还需要根据具体业务场景进行调整和优化。
记住,好的架构不是一蹴而就的,需要在实践中不断迭代和完善。希望本文能为你的Vue 3项目开发提供有价值的参考和指导。

评论 (0)