Vue 3 + TypeScript + Vite 构建高性能前端应用:从零搭建到性能优化

Oscar731
Oscar731 2026-02-27T10:04:11+08:00
0 0 0

引言

在现代前端开发中,构建高性能、可维护的应用程序已成为开发者的首要任务。Vue 3、TypeScript 和 Vite 的组合为开发者提供了强大的工具链,能够构建出既高效又可靠的前端应用。本文将从零开始,详细介绍如何使用这些技术构建现代化前端应用,并深入探讨性能优化策略。

项目初始化与环境搭建

1.1 创建项目

首先,我们需要使用 Vite 创建一个新的 Vue 3 项目。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

选择 vue-ts 模板会自动配置 TypeScript 支持。项目创建完成后,进入项目目录并安装依赖:

cd my-vue-app
npm install

1.2 项目结构分析

创建的项目结构如下:

my-vue-app/
├── public/
│   └── vite.svg
├── src/
│   ├── assets/
│   │   └── vue.svg
│   ├── components/
│   │   └── HelloWorld.vue
│   ├── views/
│   ├── router/
│   ├── store/
│   ├── styles/
│   ├── utils/
│   ├── App.vue
│   └── main.ts
├── env.d.ts
├── index.html
├── package.json
├── tsconfig.json
├── vite.config.ts
└── README.md

1.3 TypeScript 配置

tsconfig.json 中,我们通常会看到这样的配置:

{
  "compilerOptions": {
    "target": "ES2020",
    "useDefineForClassFields": true,
    "module": "ESNext",
    "moduleResolution": "Node",
    "strict": true,
    "jsx": "preserve",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "esModuleInterop": true,
    "lib": ["ES2020", "DOM", "DOM.Iterable"],
    "skipLibCheck": true,
    "noEmit": true
  },
  "include": ["src/**/*", "src/**/*.vue"]
}

这个配置确保了 TypeScript 能够正确处理 Vue 组件和现代 JavaScript 特性。

组件设计与开发

2.1 Vue 3 组件基础

Vue 3 引入了 Composition API,提供了更灵活的组件逻辑组织方式。让我们创建一个简单的用户卡片组件:

<!-- src/components/UserCard.vue -->
<template>
  <div class="user-card" :class="{ 'is-loading': loading }">
    <div v-if="loading" class="loading-placeholder">
      <div class="skeleton-line"></div>
      <div class="skeleton-line"></div>
      <div class="skeleton-line"></div>
    </div>
    
    <div v-else class="user-content">
      <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>
  </div>
</template>

<script setup lang="ts">
import { ref, defineProps, defineEmits } from 'vue'

// 定义 props
interface User {
  id: number
  name: string
  email: string
  avatar: string
}

const props = defineProps<{
  user: User
  loading?: boolean
}>()

// 定义 emits
const emit = defineEmits<{
  (e: 'edit', id: number): void
  (e: 'delete', id: number): void
}>()

// 处理编辑事件
const handleEdit = () => {
  emit('edit', props.user.id)
}

// 处理删除事件
const handleDelete = () => {
  emit('delete', props.user.id)
}
</script>

<style scoped lang="scss">
.user-card {
  border: 1px solid #e0e0e0;
  border-radius: 8px;
  padding: 16px;
  margin: 16px 0;
  background: white;
  box-shadow: 0 2px 4px rgba(0,0,0,0.1);
  
  &.is-loading {
    .user-content {
      display: none;
    }
    
    .loading-placeholder {
      .skeleton-line {
        height: 12px;
        background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
        background-size: 200% 100%;
        animation: loading 1.5s infinite;
        margin-bottom: 8px;
        border-radius: 4px;
      }
      
      .skeleton-line:last-child {
        margin-bottom: 0;
      }
    }
  }
}

.avatar {
  width: 60px;
  height: 60px;
  border-radius: 50%;
  object-fit: cover;
  margin-right: 16px;
}

.user-info {
  display: flex;
  flex-direction: column;
}

.user-name {
  margin: 0 0 8px 0;
  font-size: 18px;
  font-weight: 600;
}

.user-email {
  margin: 0 0 16px 0;
  color: #666;
  font-size: 14px;
}

.user-actions {
  display: flex;
  gap: 8px;
}

.btn {
  padding: 8px 16px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  
  &.btn-edit {
    background: #007bff;
    color: white;
  }
  
  &.btn-delete {
    background: #dc3545;
    color: white;
  }
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
</style>

2.2 组件通信机制

在 Vue 3 中,组件间通信主要通过 props、emits 和 provide/inject 实现:

<!-- src/components/ParentComponent.vue -->
<template>
  <div>
    <h2>父组件</h2>
    <UserCard 
      :user="currentUser" 
      :loading="loading"
      @edit="handleEdit"
      @delete="handleDelete"
    />
    <button @click="fetchUser">获取用户</button>
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import UserCard from './UserCard.vue'

interface User {
  id: number
  name: string
  email: string
  avatar: string
}

const currentUser = ref<User | null>(null)
const loading = ref(false)

const fetchUser = async () => {
  loading.value = true
  try {
    // 模拟 API 调用
    const response = await fetch('/api/user')
    currentUser.value = await response.json()
  } catch (error) {
    console.error('获取用户失败:', error)
  } finally {
    loading.value = false
  }
}

const handleEdit = (id: number) => {
  console.log('编辑用户:', id)
}

const handleDelete = (id: number) => {
  console.log('删除用户:', id)
  currentUser.value = null
}
</script>

状态管理

3.1 Pinia 状态管理

Pinia 是 Vue 3 推荐的状态管理库,它提供了更简洁的 API 和更好的 TypeScript 支持:

npm install pinia
// src/stores/userStore.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

export interface User {
  id: number
  name: string
  email: string
  avatar: string
}

export const useUserStore = defineStore('user', () => {
  const users = ref<User[]>([])
  const currentUser = ref<User | null>(null)
  const loading = ref(false)
  const error = ref<string | null>(null)

  // 获取用户列表
  const fetchUsers = async () => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('/api/users')
      users.value = await response.json()
    } catch (err) {
      error.value = '获取用户列表失败'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  // 获取单个用户
  const fetchUser = async (id: number) => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(`/api/users/${id}`)
      currentUser.value = await response.json()
    } catch (err) {
      error.value = '获取用户失败'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  // 创建用户
  const createUser = async (userData: Omit<User, 'id'>) => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('/api/users', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(userData),
      })
      const newUser = await response.json()
      users.value.push(newUser)
      return newUser
    } catch (err) {
      error.value = '创建用户失败'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  // 更新用户
  const updateUser = async (id: number, userData: Partial<User>) => {
    loading.value = true
    error.value = null
    try {
      const response = await fetch(`/api/users/${id}`, {
        method: 'PUT',
        headers: {
          'Content-Type': 'application/json',
        },
        body: JSON.stringify(userData),
      })
      const updatedUser = await response.json()
      const index = users.value.findIndex(user => user.id === id)
      if (index !== -1) {
        users.value[index] = updatedUser
      }
      if (currentUser.value?.id === id) {
        currentUser.value = updatedUser
      }
      return updatedUser
    } catch (err) {
      error.value = '更新用户失败'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  // 删除用户
  const deleteUser = async (id: number) => {
    loading.value = true
    error.value = null
    try {
      await fetch(`/api/users/${id}`, {
        method: 'DELETE',
      })
      users.value = users.value.filter(user => user.id !== id)
      if (currentUser.value?.id === id) {
        currentUser.value = null
      }
    } catch (err) {
      error.value = '删除用户失败'
      console.error(err)
    } finally {
      loading.value = false
    }
  }

  // 计算属性
  const userCount = computed(() => users.value.length)
  const hasError = computed(() => error.value !== null)

  return {
    users,
    currentUser,
    loading,
    error,
    userCount,
    hasError,
    fetchUsers,
    fetchUser,
    createUser,
    updateUser,
    deleteUser,
  }
})

3.2 在组件中使用状态管理

<!-- src/views/UserList.vue -->
<template>
  <div class="user-list">
    <h1>用户列表</h1>
    
    <div v-if="loading" class="loading">
      <div class="spinner"></div>
      <p>加载中...</p>
    </div>
    
    <div v-else-if="hasError" class="error">
      <p>{{ error }}</p>
      <button @click="fetchUsers">重试</button>
    </div>
    
    <div v-else>
      <p>总用户数: {{ userCount }}</p>
      <div class="users-grid">
        <UserCard 
          v-for="user in users" 
          :key="user.id"
          :user="user"
          @edit="handleEdit"
          @delete="handleDelete"
        />
      </div>
    </div>
    
    <button @click="showCreateModal = true" class="btn btn-primary">
      添加用户
    </button>
    
    <CreateUserModal 
      v-if="showCreateModal" 
      @close="showCreateModal = false"
      @created="handleUserCreated"
    />
  </div>
</template>

<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useUserStore } from '../stores/userStore'
import UserCard from '../components/UserCard.vue'
import CreateUserModal from '../components/CreateUserModal.vue'

const userStore = useUserStore()
const { users, loading, error, hasError, userCount, fetchUsers } = userStore
const showCreateModal = ref(false)

onMounted(() => {
  fetchUsers()
})

const handleEdit = (id: number) => {
  console.log('编辑用户:', id)
}

const handleDelete = async (id: number) => {
  if (confirm('确定要删除这个用户吗?')) {
    await userStore.deleteUser(id)
  }
}

const handleUserCreated = () => {
  showCreateModal.value = false
  fetchUsers()
}
</script>

<style scoped lang="scss">
.user-list {
  padding: 20px;
}

.loading {
  text-align: center;
  padding: 40px;
  
  .spinner {
    border: 4px solid #f3f3f3;
    border-top: 4px solid #3498db;
    border-radius: 50%;
    width: 40px;
    height: 40px;
    animation: spin 1s linear infinite;
    margin: 0 auto 16px;
  }
  
  @keyframes spin {
    0% { transform: rotate(0deg); }
    100% { transform: rotate(360deg); }
  }
}

.error {
  text-align: center;
  padding: 40px;
  color: #dc3545;
}

.users-grid {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  gap: 20px;
  margin: 20px 0;
}

.btn-primary {
  background: #007bff;
  color: white;
  padding: 12px 24px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  margin-top: 20px;
}
</style>

路由配置

4.1 路由设置

npm install vue-router@4
// src/router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import { useUserStore } from '../stores/userStore'

const routes = [
  {
    path: '/',
    redirect: '/users'
  },
  {
    path: '/users',
    name: 'UserList',
    component: () => import('../views/UserList.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: () => import('../views/UserDetail.vue'),
    meta: { requiresAuth: true }
  },
  {
    path: '/login',
    name: 'Login',
    component: () => import('../views/Login.vue'),
    meta: { requiresGuest: true }
  }
]

const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes
})

// 路由守卫
router.beforeEach(async (to, from, next) => {
  const userStore = useUserStore()
  
  // 检查是否需要登录
  if (to.meta.requiresAuth && !userStore.currentUser) {
    next('/login')
    return
  }
  
  // 检查是否需要未登录用户
  if (to.meta.requiresGuest && userStore.currentUser) {
    next('/users')
    return
  }
  
  next()
})

export default router

4.2 主应用配置

// src/main.ts
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
import router from './router'

const app = createApp(App)

app.use(createPinia())
app.use(router)

app.mount('#app')

性能优化策略

5.1 代码分割与懒加载

Vue 3 支持动态导入和懒加载,可以有效减少初始包大小:

// 路由中的懒加载
const routes = [
  {
    path: '/users',
    component: () => import('../views/UserList.vue')
  },
  {
    path: '/dashboard',
    component: () => import('../views/Dashboard.vue')
  }
]

5.2 组件缓存优化

使用 keep-alive 缓存组件状态:

<!-- src/App.vue -->
<template>
  <div id="app">
    <router-view v-slot="{ Component }">
      <keep-alive :include="cachedComponents">
        <component :is="Component" />
      </keep-alive>
    </router-view>
  </div>
</template>

<script setup lang="ts">
import { ref } from 'vue'

const cachedComponents = ref(['UserList', 'Dashboard'])
</script>

5.3 图片优化

实现图片懒加载和响应式图片:

<!-- src/components/LazyImage.vue -->
<template>
  <img 
    :src="src" 
    :alt="alt"
    :class="{ 'lazy-loaded': loaded, 'lazy-placeholder': !loaded }"
    @load="onLoad"
    @error="onError"
  />
</template>

<script setup lang="ts">
import { ref, onMounted, onUnmounted } from 'vue'

const props = defineProps<{
  src: string
  alt?: string
}>()

const loaded = ref(false)
const observer = ref<IntersectionObserver | null>(null)

const onLoad = () => {
  loaded.value = true
}

const onError = () => {
  console.error('图片加载失败:', props.src)
}

const handleIntersection = (entries: IntersectionObserverEntry[]) => {
  entries.forEach(entry => {
    if (entry.isIntersecting) {
      const img = entry.target as HTMLImageElement
      img.src = props.src
      observer.value?.unobserve(img)
    }
  })
}

onMounted(() => {
  if ('IntersectionObserver' in window) {
    observer.value = new IntersectionObserver(handleIntersection, {
      rootMargin: '50px'
    })
    const img = document.querySelector('img')
    if (img) {
      observer.value.observe(img)
    }
  }
})

onUnmounted(() => {
  if (observer.value) {
    observer.value.disconnect()
  }
})
</script>

<style scoped>
.lazy-loaded {
  opacity: 1;
  transition: opacity 0.3s ease;
}

.lazy-placeholder {
  opacity: 0.5;
  background: linear-gradient(90deg, #f0f0f0 25%, #e0e0e0 50%, #f0f0f0 75%);
  background-size: 200% 100%;
  animation: loading 1.5s infinite;
}

@keyframes loading {
  0% {
    background-position: 200% 0;
  }
  100% {
    background-position: -200% 0;
  }
}
</style>

5.4 状态管理优化

使用 Pinia 的持久化存储:

npm install pinia-plugin-persistedstate
// src/stores/index.ts
import { createPinia } from 'pinia'
import piniaPluginPersistedstate from 'pinia-plugin-persistedstate'

const pinia = createPinia()
pinia.use(piniaPluginPersistedstate)

export default pinia

5.5 构建优化

配置 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'),
    },
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus'],
        },
      },
    },
  },
  optimizeDeps: {
    include: ['vue', 'vue-router', 'pinia'],
  },
})

TypeScript 最佳实践

6.1 类型定义

为 API 响应和组件 props 创建严格的类型定义:

// src/types/user.ts
export interface User {
  id: number
  name: string
  email: string
  avatar: string
  createdAt: string
  updatedAt: string
}

export interface UserFormData {
  name: string
  email: string
  avatar?: string
}

export interface ApiResponse<T> {
  data: T
  message?: string
  status: number
}

6.2 泛型和工具类型

使用 TypeScript 泛型和工具类型提高代码复用性:

// src/utils/api.ts
import { User } from '../types/user'

export type ApiResponse<T> = {
  data: T
  message?: string
  status: number
}

export type PaginatedResponse<T> = ApiResponse<{
  items: T[]
  total: number
  page: number
  pageSize: number
}>

export async function fetchWithAuth<T>(
  url: string,
  options?: RequestInit
): Promise<T> {
  const token = localStorage.getItem('authToken')
  const response = await fetch(url, {
    ...options,
    headers: {
      'Content-Type': 'application/json',
      Authorization: `Bearer ${token}`,
      ...options?.headers,
    },
  })
  
  if (!response.ok) {
    throw new Error(`HTTP error! status: ${response.status}`)
  }
  
  return response.json()
}

6.3 组件类型安全

确保组件的类型安全:

<!-- src/components/UserForm.vue -->
<template>
  <form @submit.prevent="handleSubmit" class="user-form">
    <div class="form-group">
      <label for="name">姓名</label>
      <input 
        id="name" 
        v-model="form.name" 
        type="text" 
        required 
        class="form-input"
      />
    </div>
    
    <div class="form-group">
      <label for="email">邮箱</label>
      <input 
        id="email" 
        v-model="form.email" 
        type="email" 
        required 
        class="form-input"
      />
    </div>
    
    <div class="form-group">
      <label for="avatar">头像</label>
      <input 
        id="avatar" 
        v-model="form.avatar" 
        type="url" 
        class="form-input"
      />
    </div>
    
    <button type="submit" :disabled="loading" class="btn btn-primary">
      {{ loading ? '提交中...' : '提交' }}
    </button>
  </form>
</template>

<script setup lang="ts">
import { ref, defineEmits } from 'vue'
import { UserFormData } from '../types/user'

const props = defineProps<{
  loading?: boolean
}>()

const emit = defineEmits<{
  (e: 'submit', data: UserFormData): void
}>()

const form = ref<UserFormData>({
  name: '',
  email: '',
  avatar: '',
})

const handleSubmit = () => {
  emit('submit', form.value)
}
</script>

<style scoped lang="scss">
.user-form {
  max-width: 500px;
  margin: 0 auto;
}

.form-group {
  margin-bottom: 16px;
}

.form-input {
  width: 100%;
  padding: 8px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  font-size: 14px;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 14px;
  
  &.btn-primary {
    background: #007bff;
    color: white;
  }
  
  &:disabled {
    opacity: 0.6;
    cursor: not-allowed;
  }
}
</style>

测试策略

7.1 单元测试

使用 Vitest 和 Vue Test Utils 进行测试:

// src/components/UserCard.spec.ts
import { describe, it, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import UserCard from './UserCard.vue'

describe('UserCard', () => {
  const mockUser = {
    id: 1,
    name: '张三',
    email: 'zhangsan@example.com',
    avatar: '/avatar.jpg'
  }

  it('renders user information correctly', () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })

    expect(wrapper.text()).toContain('张三')
    expect(wrapper.text()).toContain('zhangsan@example.com')
    expect(wrapper.find('img').attributes('src')).toBe('/avatar.jpg')
  })

  it('emits edit event when edit button is clicked', async () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })

    await wrapper.find('.btn-edit').trigger('click')
    expect(wrapper.emitted('edit')).toBeTruthy()
    expect(wrapper.emitted('edit')?.[0]).toEqual([1])
  })

  it('emits delete event when delete button is clicked', async () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })

    await wrapper.find('.btn-delete').trigger('click')
    expect(wrapper.emitted('delete')).toBeTruthy()
    expect(wrapper.emitted('delete')?.[0]).toEqual([1])
  })
})
``
相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000