引言
在现代前端开发领域,构建高性能、可维护的应用程序已成为开发者的核心诉求。Vue 3、TypeScript和Vite 5.0的组合为这一目标提供了强大的技术支撑。本文将深入探讨如何利用这三者构建现代化的前端应用,涵盖组件设计模式、类型安全、构建优化等关键主题。
技术栈概述
Vue 3:下一代响应式框架
Vue 3作为Vue.js的最新主要版本,引入了多项革命性改进:
- Composition API:提供更灵活的代码组织方式
- 更好的TypeScript支持:原生TypeScript类型推导
- 性能优化:更小的包体积和更快的渲染速度
- 多根节点支持:组件可以返回多个根元素
TypeScript:强类型JavaScript超集
TypeScript为JavaScript添加了静态类型检查,带来:
- 编译时错误检测
- 更好的IDE支持(智能提示、重构)
- 代码可维护性提升
- 团队协作效率提高
Vite 5.0:现代化构建工具
Vite 5.0作为新一代前端构建工具,具有以下优势:
- 基于ES模块的开发服务器
- 极速热更新(HMR)
- 模块预编译优化
- 原生支持TypeScript和Vue
项目初始化与配置
使用Vite创建项目
# 使用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
核心配置文件详解
vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [
vue({
template: {
compilerOptions: {
// 配置模板编译选项
}
}
})
],
resolve: {
alias: {
'@': resolve(__dirname, './src'),
'@components': resolve(__dirname, './src/components'),
'@views': resolve(__dirname, './src/views'),
'@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: {
rollupOptions: {
output: {
manualChunks: {
vue: ['vue', 'vue-router', 'pinia'],
ui: ['element-plus', '@element-plus/icons-vue']
}
}
},
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
}
})
tsconfig.json
{
"compilerOptions": {
"target": "ES2020",
"useDefineForClassFields": true,
"module": "ESNext",
"moduleResolution": "Node",
"strict": true,
"skipLibCheck": true,
"lib": ["ES2020", "DOM", "DOM.Iterable"],
"types": ["vite/client"],
"allowSyntheticDefaultImports": true,
"esModuleInterop": true,
"resolveJsonModule": true,
"noEmit": true,
"jsx": "preserve",
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"],
"@views/*": ["src/views/*"],
"@services/*": ["src/services/*"],
"@utils/*": ["src/utils/*"]
}
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}
组件设计模式
使用Composition API构建组件
// src/components/UserProfile.vue
<template>
<div class="user-profile">
<h2>{{ user.name }}</h2>
<p>{{ user.email }}</p>
<button @click="updateProfile">更新资料</button>
<div v-if="loading">加载中...</div>
</div>
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { User } from '@/types/user'
// 定义props
const props = defineProps<{
userId: string
}>()
// 定义emits
const emit = defineEmits<{
(e: 'user-updated', user: User): void
}>()
// 响应式状态
const user = ref<User | null>(null)
const loading = ref(false)
// 组合逻辑
const fetchUser = async () => {
try {
loading.value = true
// 模拟API调用
const response = await fetch(`/api/users/${props.userId}`)
user.value = await response.json()
} catch (error) {
console.error('获取用户信息失败:', error)
} finally {
loading.value = false
}
}
const updateProfile = () => {
if (user.value) {
emit('user-updated', user.value)
}
}
// 组件生命周期
onMounted(() => {
fetchUser()
})
</script>
<style scoped>
.user-profile {
padding: 20px;
border: 1px solid #ddd;
border-radius: 8px;
}
</style>
自定义Hook设计模式
// src/composables/useApi.ts
import { ref, Ref } from 'vue'
export function useApi<T>(url: string) {
const data = ref<T | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const fetchData = async () => {
try {
loading.value = true
error.value = null
const response = await fetch(url)
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`)
}
data.value = await response.json()
} catch (err) {
error.value = err as Error
console.error('API请求失败:', err)
} finally {
loading.value = false
}
}
return {
data,
loading,
error,
fetchData
}
}
// src/composables/useLocalStorage.ts
import { ref, watch } from 'vue'
export function useLocalStorage<T>(key: string, defaultValue: T) {
const storedValue = localStorage.getItem(key)
const value = ref<T>(storedValue ? JSON.parse(storedValue) : defaultValue)
watch(value, (newValue) => {
localStorage.setItem(key, JSON.stringify(newValue))
}, { deep: true })
return value
}
高级组件通信模式
// src/components/ParentComponent.vue
<template>
<div class="parent-component">
<ChildComponent
:user-data="userData"
@update-user="handleUserUpdate"
/>
<div>用户状态: {{ userStatus }}</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import ChildComponent from './ChildComponent.vue'
const userData = ref({
name: '张三',
email: 'zhangsan@example.com'
})
const userStatus = computed(() => {
return userData.value.email ? '已激活' : '未激活'
})
const handleUserUpdate = (updatedData: any) => {
userData.value = updatedData
}
</script>
// src/components/ChildComponent.vue
<template>
<div class="child-component">
<input v-model="localData.name" placeholder="姓名" />
<input v-model="localData.email" placeholder="邮箱" />
<button @click="updateParent">更新父组件</button>
</div>
</template>
<script setup lang="ts">
import { ref, watch } from 'vue'
const props = defineProps<{
userData: {
name: string
email: string
}
}>()
const emit = defineEmits<{
(e: 'update-user', data: any): void
}>()
const localData = ref({ ...props.userData })
// 同步父组件数据变化
watch(() => props.userData, (newVal) => {
localData.value = { ...newVal }
})
const updateParent = () => {
emit('update-user', localData.value)
}
</script>
TypeScript类型安全实践
接口和类型定义
// src/types/user.ts
export interface User {
id: string
name: string
email: string
avatar?: string
role: 'admin' | 'user' | 'guest'
createdAt: Date
updatedAt: Date
}
export interface UserListResponse {
users: User[]
total: number
page: number
pageSize: number
}
export type UserRole = 'admin' | 'user' | 'guest'
// src/types/api.ts
export interface ApiResponse<T> {
code: number
message: string
data: T
timestamp: Date
}
export type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
export interface ApiRequestConfig {
method?: HttpMethod
headers?: Record<string, string>
params?: Record<string, any>
body?: any
}
泛型和条件类型
// src/utils/typeUtils.ts
// 条件类型示例
type NonNullable<T> = T extends null | undefined ? never : T
type Nullable<T> = T | null | undefined
// 泛型组件类型
interface AsyncComponentProps<T> {
data: T | null
loading: boolean
error: Error | null
}
// 条件渲染类型
type ComponentType<T extends boolean> = T extends true
? 'div'
: 'span'
// 索引签名示例
interface UserConfig {
[key: string]: any
name: string
age: number
}
// 函数类型定义
type AsyncFunction<T> = (...args: any[]) => Promise<T>
type EventHandler<T = void> = (event: Event) => T
// 实际应用示例
const processUser = <T extends User>(user: T): T => {
return { ...user, updatedAt: new Date() }
}
const validateUser = <T extends Partial<User>>(userData: T): T & Required<Pick<User, 'name' | 'email'>> => {
if (!userData.name || !userData.email) {
throw new Error('姓名和邮箱不能为空')
}
return { ...userData, name: userData.name!, email: userData.email! } as T & Required<Pick<User, 'name' | 'email'>>
}
类型守卫和类型断言
// src/utils/typeGuards.ts
import { User, UserRole } from '@/types/user'
// 类型守卫函数
export function isUser(obj: any): obj is User {
return (
typeof obj === 'object' &&
obj !== null &&
typeof obj.id === 'string' &&
typeof obj.name === 'string' &&
typeof obj.email === 'string' &&
typeof obj.role === 'string' &&
['admin', 'user', 'guest'].includes(obj.role)
)
}
export function isUserRole(role: string): role is UserRole {
return ['admin', 'user', 'guest'].includes(role)
}
// 实际使用示例
const handleUser = (data: any) => {
if (isUser(data)) {
// TypeScript会自动推断为User类型
console.log(data.name, data.email)
}
if (isUserRole(data.role)) {
// TypeScript会自动推断为UserRole类型
console.log(`用户角色: ${data.role}`)
}
}
// 类型断言示例
const getElementById = (id: string): HTMLElement => {
const element = document.getElementById(id)
if (!element) {
throw new Error(`Element with id '${id}' not found`)
}
return element as HTMLElement // 类型断言
}
状态管理与Pinia
Pinia基础配置
// src/stores/userStore.ts
import { defineStore } from 'pinia'
import { User } from '@/types/user'
export const useUserStore = defineStore('user', {
state: () => ({
currentUser: null as User | null,
isLoggedIn: false,
loading: false
}),
getters: {
userName: (state) => state.currentUser?.name || '访客',
userRole: (state) => state.currentUser?.role || 'guest',
hasPermission: (state) => (requiredRole: string) => {
if (!state.currentUser) return false
const roleOrder = ['guest', 'user', 'admin']
const currentUserRoleIndex = roleOrder.indexOf(state.currentUser.role)
const requiredRoleIndex = roleOrder.indexOf(requiredRole)
return currentUserRoleIndex >= requiredRoleIndex
}
},
actions: {
async login(email: string, password: string) {
this.loading = true
try {
const response = await fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ email, password })
})
if (!response.ok) {
throw new Error('登录失败')
}
const userData = await response.json()
this.currentUser = userData.user
this.isLoggedIn = true
return userData
} catch (error) {
console.error('登录错误:', error)
throw error
} finally {
this.loading = false
}
},
logout() {
this.currentUser = null
this.isLoggedIn = false
}
}
})
高级Pinia模式
// src/stores/createStore.ts
import { defineStore } from 'pinia'
import { Ref, computed, watch } from 'vue'
interface StoreState<T> {
data: T | null
loading: boolean
error: Error | null
}
export function createBaseStore<T>(name: string) {
return defineStore(name, {
state: (): StoreState<T> => ({
data: null,
loading: false,
error: null
}),
getters: {
hasData: (state) => !!state.data,
isEmpty: (state) => !state.data && !state.loading
},
actions: {
setLoading(loading: boolean) {
this.loading = loading
},
setError(error: Error | null) {
this.error = error
},
setData(data: T | null) {
this.data = data
}
}
})
}
// 使用示例
export const useUserListStore = createBaseStore<User[]>('userList')
性能优化策略
组件懒加载与代码分割
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { defineAsyncComponent } from 'vue'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'About',
component: defineAsyncComponent(() => import('@/views/About.vue'))
},
{
path: '/users',
name: 'Users',
component: () => import('@/views/Users.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
虚拟滚动实现
// 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="{ transform: `translateY(${item.offset}px)` }"
>
{{ item.data }}
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed, onMounted, watch } from 'vue'
const props = defineProps<{
items: any[]
itemHeight: number
}>()
const containerRef = ref<HTMLElement | null>(null)
const scrollTop = ref(0)
const containerHeight = ref(0)
// 计算可见项目
const visibleItems = computed(() => {
if (!containerRef.value) return []
const startIndex = Math.floor(scrollTop.value / props.itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight.value / props.itemHeight) + 1,
props.items.length
)
return props.items.slice(startIndex, endIndex).map((item, index) => ({
id: item.id,
data: item,
offset: (startIndex + index) * props.itemHeight
}))
})
const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
const handleScroll = () => {
if (containerRef.value) {
scrollTop.value = containerRef.value.scrollTop
}
}
onMounted(() => {
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight
containerRef.value.addEventListener('scroll', handleScroll)
}
})
watch(
() => props.items,
() => {
// 当数据变化时重新计算高度
if (containerRef.value) {
containerHeight.value = containerRef.value.clientHeight
}
}
)
// 清理事件监听器
onUnmounted(() => {
if (containerRef.value) {
containerRef.value.removeEventListener('scroll', handleScroll)
}
})
</script>
<style scoped>
.virtual-list {
height: 400px;
overflow-y: auto;
}
.virtual-list-container {
position: relative;
}
.virtual-item {
position: absolute;
width: 100%;
box-sizing: border-box;
}
</style>
缓存策略
// src/utils/cache.ts
import { ref, watch } from 'vue'
export class CacheManager {
private cache = new Map<string, any>()
private ttlMap = new Map<string, number>()
set<T>(key: string, value: T, ttl: number = 300000): void {
this.cache.set(key, value)
this.ttlMap.set(key, Date.now() + ttl)
}
get<T>(key: string): T | undefined {
const now = Date.now()
const ttl = this.ttlMap.get(key)
if (ttl && now > ttl) {
this.cache.delete(key)
this.ttlMap.delete(key)
return undefined
}
return this.cache.get(key)
}
has(key: string): boolean {
return this.cache.has(key)
}
delete(key: string): boolean {
this.cache.delete(key)
this.ttlMap.delete(key)
return true
}
clear(): void {
this.cache.clear()
this.ttlMap.clear()
}
}
// 使用示例
const cache = new CacheManager()
export const useCachedApi = <T>(key: string, apiCall: () => Promise<T>) => {
const data = ref<T | null>(null)
const loading = ref(false)
const fetchData = async () => {
loading.value = true
// 尝试从缓存获取
const cachedData = cache.get<T>(key)
if (cachedData) {
data.value = cachedData
loading.value = false
return cachedData
}
try {
const result = await apiCall()
cache.set(key, result, 300000) // 缓存5分钟
data.value = result
return result
} catch (error) {
console.error('API调用失败:', error)
throw error
} finally {
loading.value = false
}
}
return { data, loading, fetchData }
}
构建优化策略
Vite构建配置优化
// vite.config.ts - 生产环境优化
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
import { visualizer } from 'rollup-plugin-visualizer'
export default defineConfig(({ mode }) => {
const isProduction = mode === 'production'
return {
plugins: [
vue(),
isProduction && visualizer({
filename: "dist/stats.html",
open: false,
gzipSize: true,
brotliSize: true
})
],
build: {
// 输出目录
outDir: 'dist',
// 资源路径
assetsDir: 'assets',
// 构建目标
target: 'es2020',
// 模块预编译优化
rollupOptions: {
output: {
// 代码分割策略
manualChunks: {
vendor: ['vue', 'vue-router', 'pinia', 'axios'],
ui: ['element-plus', '@element-plus/icons-vue'],
utils: ['lodash-es', 'dayjs']
},
// 静态资源处理
assetFileNames: (assetInfo) => {
if (assetInfo.name.endsWith('.css')) {
return 'assets/[name].[hash].[ext]'
}
return 'assets/[name].[hash].[ext]'
}
}
},
// 压缩配置
terserOptions: {
compress: {
// 移除console
drop_console: true,
// 移除debugger
drop_debugger: true,
// 简化条件表达式
pure_funcs: ['console.log', 'console.warn']
},
// 保留注释
format: {
comments: false
}
},
// 预加载配置
cssCodeSplit: true,
sourcemap: false
},
// 预加载优化
optimizeDeps: {
include: ['vue', 'vue-router', 'pinia'],
exclude: ['@vueuse/core']
}
}
})
资源优化策略
// src/utils/resourceOptimizer.ts
import { ref } from 'vue'
export class ResourceOptimizer {
private static instance: ResourceOptimizer
private constructor() {}
static getInstance(): ResourceOptimizer {
if (!ResourceOptimizer.instance) {
ResourceOptimizer.instance = new ResourceOptimizer()
}
return ResourceOptimizer.instance
}
// 图片懒加载优化
static lazyLoadImages() {
const images = document.querySelectorAll('img[data-src]')
const observer = new IntersectionObserver((entries) => {
entries.forEach(entry => {
if (entry.isIntersecting) {
const img = entry.target as HTMLImageElement
img.src = img.dataset.src || ''
img.removeAttribute('data-src')
observer.unobserve(img)
}
})
})
images.forEach(img => observer.observe(img))
}
// 字体优化
static optimizeFonts() {
// 预加载关键字体
const fontPreload = document.createElement('link')
fontPreload.rel = 'preload'
fontPreload.as = 'font'
fontPreload.href = '/fonts/main-font.woff2'
fontPreload.crossOrigin = 'anonymous'
document.head.appendChild(fontPreload)
}
// 预加载关键资源
static preloadCriticalResources() {
const criticalResources = [
{ rel: 'preload', as: 'script', href: '/assets/main.js' },
{ rel: 'preload', as: 'style', href: '/assets/main.css' }
]
criticalResources.forEach(resource => {
const link = document.createElement('link')
Object.assign(link, resource)
document.head.appendChild(link)
})
}
}
// 在main.ts中使用
import { ResourceOptimizer } from '@/utils/resourceOptimizer'
// 应用启动时执行优化
ResourceOptimizer.preloadCriticalResources()
ResourceOptimizer.optimizeFonts()
// 延迟执行图片懒加载
setTimeout(() => {
ResourceOptimizer.lazyLoadImages()
}, 1000)
测试策略
单元测试配置
// src/__tests__/userStore.test.ts
import { describe, it, expect, vi } from 'vitest'
import { useUserStore } from '@/stores/userStore'
describe('User Store', () => {
it('should initialize with default values', () => {
const store = useUserStore()
expect(store.currentUser).toBeNull()
expect(store.isLoggedIn).toBe(false)
expect(store.loading).toBe(false)
})
it('should set user data correctly', () => {
const store = useUserStore()
const userData = {
id: '1',
name: '测试用户',
email: 'test@example.com',
role: 'user' as const
}
store.currentUser = userData
expect(store.currentUser).toEqual(userData)
})
it('should handle login correctly', async () => {
const store = useUserStore()
// Mock fetch
global.fetch = vi.fn().mockResolvedValue({
ok: true,
json: () => Promise.resolve({ user: { id: '1', name: '测试用户' } })
})
await store.login('test@example.com', 'password')
expect(store.isLoggedIn).toBe(true)
expect(store.currentUser?.name).toBe('测试用户')
})
})
组件测试
// src/__tests__/UserProfile.test.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserProfile from '@/components/UserProfile.vue'
describe('UserProfile Component', () => {
const mockUser = {
id: '1',
name: '张三',
email: 'zhangsan@example.com',
role: 'user' as const
}
it('renders user data correctly', () => {
const wrapper = mount(UserProfile, {
props: {
userId: '1'
}
})
expect(wrapper.text()).toContain('张三')
expect(wrapper.text()).toContain('zhangsan@example.com')
})
it('emits update-user event when button is clicked', async () => {
const wrapper = mount(UserProfile, {
props: {
userId: '1'
}
})
await wrapper.find('button').trigger('click')
expect(wrapper.emitted('user-updated')).toBeTruthy()
})
})
部署与CI/CD
构建脚本优化
// package.json
{
"scripts": {
"dev": "vite",
"build": "vite build",
"build:analyze": "vite build --mode production --report
评论 (0)