前言
随着前端技术的快速发展,Vue 3作为新一代的前端框架,结合TypeScript和Vite构建工具,已经成为了企业级应用开发的主流选择。本文将系统性地分享在Vue 3生态下进行企业级项目开发的最佳实践,涵盖从项目搭建到性能优化的完整流程。
Vue 3 + TypeScript + Vite技术栈优势
Vue 3的核心特性
Vue 3基于Composition API重构,提供了更灵活的组件逻辑组织方式。相比Vue 2的Options API,Composition API让代码更加模块化和可复用。同时,Vue 3在性能上也有显著提升,包括更小的包体积、更快的渲染速度等。
TypeScript的优势
TypeScript为JavaScript添加了静态类型检查,在大型项目中能够有效减少运行时错误,提高开发效率。对于企业级应用而言,类型安全能够确保团队协作的一致性,降低维护成本。
Vite构建工具的特点
Vite作为新一代构建工具,利用浏览器原生ES模块特性,提供了极快的开发服务器启动速度和热更新体验。相比传统的webpack构建工具,Vite在开发阶段的响应速度提升了数倍。
项目初始化与配置
使用Vite创建Vue 3项目
# 使用npm
npm create vite@latest my-vue-app --template vue-ts
# 使用yarn
yarn create vite my-vue-app --template vue-ts
# 使用pnpm
pnpm create vite my-vue-app --template vue-ts
项目结构规划
my-vue-app/
├── public/
│ └── favicon.ico
├── src/
│ ├── assets/ # 静态资源
│ ├── components/ # 公共组件
│ ├── composables/ # 可复用逻辑
│ ├── views/ # 页面组件
│ ├── router/ # 路由配置
│ ├── store/ # 状态管理
│ ├── services/ # API服务
│ ├── utils/ # 工具函数
│ ├── types/ # 类型定义
│ ├── App.vue
│ └── main.ts
├── tests/
├── .env.* # 环境变量配置
├── vite.config.ts # Vite配置
└── tsconfig.json # TypeScript配置
TypeScript配置优化
// tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"skipLibCheck": true,
"esModuleInterop": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"useDefineForClassFields": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"baseUrl": ".",
"paths": {
"@/*": ["src/*"]
}
},
"include": ["src/**/*", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
}
组件设计规范
组件结构标准化
<!-- src/components/UserCard.vue -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" class="avatar" />
<div class="user-info">
<h3 class="user-name">{{ user.name }}</h3>
<p class="user-email">{{ user.email }}</p>
<div class="user-actions">
<button @click="handleEdit" class="btn btn-edit">编辑</button>
<button @click="handleDelete" class="btn btn-delete">删除</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
// 定义props类型
interface User {
id: number
name: string
email: string
avatar: string
}
const props = defineProps<{
user: User
}>()
// 定义emit事件
const emit = defineEmits<{
(e: 'edit', user: User): void
(e: 'delete', userId: number): void
}>()
// 处理编辑事件
const handleEdit = () => {
emit('edit', props.user)
}
// 处理删除事件
const handleDelete = () => {
emit('delete', props.user.id)
}
</script>
<style scoped lang="scss">
.user-card {
display: flex;
align-items: center;
padding: 16px;
border: 1px solid #e0e0e0;
border-radius: 8px;
margin-bottom: 16px;
.avatar {
width: 50px;
height: 50px;
border-radius: 50%;
margin-right: 16px;
}
.user-info {
flex: 1;
.user-name {
margin: 0 0 8px 0;
font-size: 16px;
font-weight: bold;
}
.user-email {
margin: 0 0 12px 0;
color: #666;
font-size: 14px;
}
.user-actions {
display: flex;
gap: 8px;
.btn {
padding: 6px 12px;
border: none;
border-radius: 4px;
cursor: pointer;
&.btn-edit {
background-color: #007bff;
color: white;
}
&.btn-delete {
background-color: #dc3545;
color: white;
}
}
}
}
}
</style>
组件通信最佳实践
<!-- src/components/Modal.vue -->
<template>
<div v-if="visible" class="modal-overlay">
<div class="modal-content">
<div class="modal-header">
<h3>{{ title }}</h3>
<button @click="handleClose" class="close-btn">×</button>
</div>
<div class="modal-body">
<slot></slot>
</div>
<div class="modal-footer">
<button @click="handleCancel" class="btn btn-secondary">取消</button>
<button @click="handleConfirm" class="btn btn-primary">确认</button>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { defineProps, defineEmits } from 'vue'
const props = defineProps<{
visible: boolean
title?: string
}>()
const emit = defineEmits<{
(e: 'close'): void
(e: 'cancel'): void
(e: 'confirm'): void
}>()
const handleClose = () => {
emit('close')
}
const handleCancel = () => {
emit('cancel')
}
const handleConfirm = () => {
emit('confirm')
}
</script>
状态管理设计
Pinia状态管理
// src/store/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { User } from '@/types/user'
export const useUserStore = defineStore('user', () => {
// 状态
const users = ref<User[]>([])
const currentUser = ref<User | null>(null)
const loading = ref(false)
// 计算属性
const userCount = computed(() => users.value.length)
const isLoggedIn = computed(() => !!currentUser.value)
// 动作
const fetchUsers = async () => {
loading.value = true
try {
const response = await fetch('/api/users')
users.value = await response.json()
} catch (error) {
console.error('获取用户失败:', error)
} finally {
loading.value = false
}
}
const setCurrentUser = (user: User | null) => {
currentUser.value = user
}
const addUser = (user: User) => {
users.value.push(user)
}
const updateUser = (updatedUser: User) => {
const index = users.value.findIndex(u => u.id === updatedUser.id)
if (index !== -1) {
users.value[index] = updatedUser
}
}
const deleteUser = (userId: number) => {
users.value = users.value.filter(user => user.id !== userId)
}
return {
users,
currentUser,
loading,
userCount,
isLoggedIn,
fetchUsers,
setCurrentUser,
addUser,
updateUser,
deleteUser
}
})
复用逻辑封装
// src/composables/useApi.ts
import { ref, reactive } from 'vue'
import type { Ref } from 'vue'
export function useApi<T>() {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const execute = async (apiCall: () => Promise<T>) => {
loading.value = true
error.value = null
try {
data.value = await apiCall()
} catch (err) {
error.value = err as Error
console.error('API调用失败:', err)
} finally {
loading.value = false
}
}
const reset = () => {
data.value = null
loading.value = false
error.value = null
}
return {
data,
loading,
error,
execute,
reset
}
}
// 使用示例
export function useUserList() {
const { data: users, loading, error, execute } = useApi<User[]>()
const fetchUsers = async () => {
await execute(async () => {
const response = await fetch('/api/users')
return response.json()
})
}
return {
users,
loading,
error,
fetchUsers
}
}
路由配置与权限控制
路由结构设计
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '@/store/userStore'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/HomeView.vue'),
meta: { requiresAuth: false }
},
{
path: '/login',
name: 'Login',
component: () => import('@/views/LoginView.vue'),
meta: { requiresAuth: false }
},
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { requiresAuth: true }
},
{
path: '/users',
name: 'Users',
component: () => import('@/views/UsersView.vue'),
meta: { requiresAuth: true, permission: 'user:read' }
},
{
path: '/admin',
name: 'Admin',
component: () => import('@/views/AdminView.vue'),
meta: { requiresAuth: true, permission: 'admin:access' }
}
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
const userStore = useUserStore()
if (to.meta.requiresAuth && !userStore.isLoggedIn) {
next('/login')
return
}
// 权限检查
if (to.meta.permission && !userStore.currentUser?.permissions?.includes(to.meta.permission)) {
next('/unauthorized')
return
}
next()
})
export default router
权限控制组件
<!-- src/components/PermissionGuard.vue -->
<template>
<div v-if="hasPermission">
<slot></slot>
</div>
<div v-else class="permission-denied">
<p>您没有权限访问此内容</p>
</div>
</template>
<script setup lang="ts">
import { computed } from 'vue'
import { useUserStore } from '@/store/userStore'
const props = defineProps<{
permission: string
}>()
const userStore = useUserStore()
const hasPermission = computed(() => {
return userStore.currentUser?.permissions?.includes(props.permission) || false
})
</script>
<style scoped>
.permission-denied {
padding: 20px;
text-align: center;
color: #666;
background-color: #f5f5f5;
border-radius: 4px;
}
</style>
API服务封装
请求拦截器设计
// src/services/apiClient.ts
import axios, { AxiosInstance, AxiosRequestConfig, AxiosResponse } from 'axios'
import { useUserStore } from '@/store/userStore'
class ApiClient {
private client: AxiosInstance
constructor() {
this.client = axios.create({
baseURL: import.meta.env.VITE_API_BASE_URL || '/api',
timeout: 10000,
headers: {
'Content-Type': 'application/json'
}
})
this.setupInterceptors()
}
private setupInterceptors() {
// 请求拦截器
this.client.interceptors.request.use(
(config) => {
const userStore = useUserStore()
const token = userStore.currentUser?.token
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
},
(error) => {
return Promise.reject(error)
}
)
// 响应拦截器
this.client.interceptors.response.use(
(response: AxiosResponse) => {
return response.data
},
(error) => {
if (error.response?.status === 401) {
const userStore = useUserStore()
userStore.setCurrentUser(null)
window.location.href = '/login'
}
return Promise.reject(error)
}
)
}
get<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.get<T>(url, config)
}
post<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.post<T>(url, data, config)
}
put<T>(url: string, data?: any, config?: AxiosRequestConfig): Promise<T> {
return this.client.put<T>(url, data, config)
}
delete<T>(url: string, config?: AxiosRequestConfig): Promise<T> {
return this.client.delete<T>(url, config)
}
}
export const apiClient = new ApiClient()
服务层封装
// src/services/userService.ts
import { apiClient } from './apiClient'
import type { User } from '@/types/user'
class UserService {
async getUsers(): Promise<User[]> {
return apiClient.get<User[]>('/users')
}
async getUserById(id: number): Promise<User> {
return apiClient.get<User>(`/users/${id}`)
}
async createUser(userData: Omit<User, 'id'>): Promise<User> {
return apiClient.post<User>('/users', userData)
}
async updateUser(id: number, userData: Partial<User>): Promise<User> {
return apiClient.put<User>(`/users/${id}`, userData)
}
async deleteUser(id: number): Promise<void> {
await apiClient.delete<void>(`/users/${id}`)
}
async searchUsers(query: string): Promise<User[]> {
return apiClient.get<User[]>(`/users/search?q=${query}`)
}
}
export const userService = new UserService()
TypeScript类型定义
基础类型定义
// src/types/user.ts
export interface User {
id: number
name: string
email: string
avatar?: string
createdAt: string
updatedAt: string
permissions?: string[]
}
export interface LoginCredentials {
email: string
password: string
}
export interface AuthResponse {
token: string
user: User
}
export interface PaginatedResponse<T> {
data: T[]
pagination: {
page: number
limit: number
total: number
totalPages: number
}
}
复杂类型定义
// src/types/form.ts
export type FormField = {
name: string
label: string
type: 'text' | 'email' | 'password' | 'select' | 'textarea' | 'date'
required?: boolean
placeholder?: string
options?: { value: string; label: string }[]
rules?: (value: any) => boolean | string
}
export type FormState<T> = {
[K in keyof T]: {
value: T[K]
error: string | null
touched: boolean
}
}
export type FormErrors<T> = Partial<Record<keyof T, string>>
export interface ValidationResult {
isValid: boolean
errors: Record<string, string>
}
性能优化策略
组件懒加载
// src/router/index.ts
const routes = [
{
path: '/dashboard',
name: 'Dashboard',
component: () => import('@/views/DashboardView.vue'),
meta: { requiresAuth: true }
},
{
path: '/reports',
name: 'Reports',
component: () => import('@/views/ReportsView.vue'),
meta: { requiresAuth: true }
},
{
path: '/analytics',
name: 'Analytics',
component: () => import('@/views/AnalyticsView.vue'),
meta: { requiresAuth: true }
}
]
虚拟滚动优化
<!-- src/components/VirtualList.vue -->
<template>
<div class="virtual-list" ref="containerRef">
<div
class="virtual-list-container"
:style="{ height: totalHeight + 'px' }"
>
<div
class="virtual-item"
v-for="item in visibleItems"
:key="item.id"
:style="{
position: 'absolute',
top: item.top + 'px',
height: itemHeight + 'px'
}"
>
<component
:is="itemComponent"
:data="item.data"
/>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
const props = defineProps<{
items: any[]
itemHeight: number
itemComponent: any
}>()
const containerRef = ref<HTMLDivElement | null>(null)
const scrollTop = ref(0)
const visibleItemCount = computed(() => {
if (!containerRef.value) return 0
return Math.ceil(containerRef.value.clientHeight / props.itemHeight)
})
const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
const startIndex = computed(() => {
return Math.floor(scrollTop.value / props.itemHeight)
})
const endIndex = computed(() => {
return Math.min(startIndex.value + visibleItemCount.value, props.items.length)
})
const visibleItems = computed(() => {
const start = startIndex.value
const end = endIndex.value
return props.items.slice(start, end).map((item, index) => ({
id: item.id,
data: item,
top: (start + index) * props.itemHeight
}))
})
const handleScroll = () => {
if (containerRef.value) {
scrollTop.value = containerRef.value.scrollTop
}
}
onMounted(() => {
if (containerRef.value) {
containerRef.value.addEventListener('scroll', handleScroll)
}
})
watch(() => props.items, () => {
// 当数据变化时重置滚动位置
scrollTop.value = 0
})
</script>
缓存策略
// src/utils/cache.ts
class Cache<T> {
private cache = new Map<string, { data: T; timestamp: number }>()
private maxSize: number
private ttl: number
constructor(maxSize = 100, ttl = 5 * 60 * 1000) {
this.maxSize = maxSize
this.ttl = ttl
}
set(key: string, data: T): void {
if (this.cache.size >= this.maxSize) {
const firstKey = this.cache.keys().next().value
this.cache.delete(firstKey)
}
this.cache.set(key, {
data,
timestamp: Date.now()
})
}
get(key: string): T | null {
const item = this.cache.get(key)
if (!item) return null
if (Date.now() - item.timestamp > this.ttl) {
this.cache.delete(key)
return null
}
return item.data
}
has(key: string): boolean {
return this.cache.has(key)
}
clear(): void {
this.cache.clear()
}
}
export const memoryCache = new Cache<any>()
构建优化配置
Vite配置优化
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
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()],
})
],
resolve: {
alias: {
'@': resolve(__dirname, './src')
}
},
build: {
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia', 'axios'],
ui: ['element-plus'],
utils: ['lodash-es', 'dayjs']
}
}
},
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
css: {
preprocessorOptions: {
scss: {
additionalData: `@import "@/styles/variables.scss";`
}
}
}
})
Tree Shaking优化
// src/utils/index.ts
// 按需导入,避免全量引入
export { debounce, throttle } from 'lodash-es'
export { formatCurrency } from './format'
export { validateEmail } from './validation'
// 按需导入组件
export { default as Button } from '@/components/Button.vue'
export { default as Input } from '@/components/Input.vue'
测试策略
单元测试配置
// src/__tests__/userStore.test.ts
import { describe, it, expect, beforeEach } from 'vitest'
import { useUserStore } from '@/store/userStore'
describe('User Store', () => {
let store: ReturnType<typeof useUserStore>
beforeEach(() => {
store = useUserStore()
})
it('should initialize with empty users and loading false', () => {
expect(store.users).toHaveLength(0)
expect(store.loading).toBe(false)
})
it('should add user correctly', () => {
const user = { id: 1, name: 'John', email: 'john@example.com' }
store.addUser(user)
expect(store.users).toHaveLength(1)
expect(store.users[0]).toEqual(user)
})
it('should update user correctly', () => {
const initialUser = { id: 1, name: 'John', email: 'john@example.com' }
const updatedUser = { id: 1, name: 'Jane', email: 'jane@example.com' }
store.addUser(initialUser)
store.updateUser(updatedUser)
expect(store.users[0]).toEqual(updatedUser)
})
})
组件测试
<!-- src/__tests__/UserCard.test.vue -->
<template>
<div>
<UserCard
:user="testUser"
@edit="handleEdit"
@delete="handleDelete"
/>
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import UserCard from '@/components/UserCard.vue'
const testUser = {
id: 1,
name: 'John Doe',
email: 'john@example.com',
avatar: '/avatar.jpg'
}
const handleEdit = vi.fn()
const handleDelete = vi.fn()
// 测试组件渲染
it('renders user card correctly', () => {
const { getByText, getByAltText } = render(UserCard, {
props: {
user: testUser
}
})
expect(getByText('John Doe')).toBeInTheDocument()
expect(getByText('john@example.com')).toBeInTheDocument()
expect(getByAltText('John Doe')).toBeInTheDocument()
})
// 测试事件触发
it('emits edit event when edit button is clicked', () => {
const { getByText } = render(UserCard, {
props: {
user: testUser
}
})
fireEvent.click(getByText('编辑'))
expect(handleEdit).toHaveBeenCalledWith(testUser)
})
</script>
部署与CI/CD
构建脚本优化
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:preview": "vite build --mode preview",
"build:analyze": "vite-bundle-visualizer",
"test": "vitest",
"test:coverage": "vitest run --coverage",
"lint": "eslint src --ext .ts,.vue --fix",
"type-check": "tsc --noEmit"
}
}
部署配置
# .github/workflows/deploy.yml
name: Deploy to Production
on:
push:
branches: [main]
jobs:
build-and-deploy:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v3
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm ci
- name: Run tests
run: npm run test
- name: Type checking
run: npm run type-check
- name: Build
run: npm run build
- name: Deploy to production
uses: peaceiris/actions-gh-pages@v3
with:
github_token: ${{ secrets.GITHUB_TOKEN }}
publish_dir: ./dist
总结
Vue 3 + TypeScript + Vite的技术栈为企业级应用开发提供了强大的技术支持。通过合理的项目结构设计、组件规范制定

评论 (0)