Vue 3 + TypeScript + Vite 构建现代化前端应用完整教程

Yvonne276
Yvonne276 2026-02-14T04:09:07+08:00
0 0 0

前言

在现代前端开发中,构建高性能、易维护的应用程序已成为开发者的首要任务。Vue 3、TypeScript 和 Vite 的组合为现代前端开发提供了完美的解决方案。本文将从零开始,详细介绍如何使用这些技术构建现代化的前端应用,涵盖从项目初始化到生产环境部署的完整流程。

项目初始化与环境搭建

1.1 使用 Vite 创建 Vue 3 项目

Vite 是新一代的前端构建工具,它提供了更快的开发服务器启动速度和更高效的热更新机制。我们首先使用 Vite 创建一个 Vue 3 项目:

npm create vite@latest my-vue-app -- --template vue-ts
cd my-vue-app
npm install

或者使用 yarn:

yarn create vite my-vue-app -- --template vue-ts
cd my-vue-app
yarn

1.2 项目结构分析

创建完成后,项目结构如下:

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

TypeScript 配置与类型系统

2.1 TypeScript 基础配置

tsconfig.json 文件中,我们配置了 TypeScript 的基础选项:

{
  "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/**/*.ts", "src/**/*.tsx", "src/**/*.vue"]
}

2.2 Vue 组件类型定义

在 Vue 3 中,我们可以通过 TypeScript 定义组件的 props 和 emits:

// components/UserCard.vue
import { defineComponent, PropType } from 'vue'

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

export default defineComponent({
  name: 'UserCard',
  props: {
    user: {
      type: Object as PropType<User>,
      required: true
    },
    showEmail: {
      type: Boolean,
      default: false
    }
  },
  emits: {
    'user-click': (userId: number) => true,
    'user-delete': (userId: number) => true
  },
  setup(props, { emit }) {
    const handleUserClick = () => {
      emit('user-click', props.user.id)
    }

    const handleDelete = () => {
      emit('user-delete', props.user.id)
    }

    return {
      handleUserClick,
      handleDelete
    }
  }
})

组件开发与最佳实践

3.1 组件设计模式

Vue 3 提供了 Composition API,让我们可以更好地组织组件逻辑:

// composables/useCounter.ts
import { ref, computed } from 'vue'

export function useCounter(initialValue = 0) {
  const count = ref(initialValue)
  
  const increment = () => {
    count.value++
  }
  
  const decrement = () => {
    count.value--
  }
  
  const reset = () => {
    count.value = initialValue
  }
  
  const doubleCount = computed(() => count.value * 2)
  
  return {
    count,
    increment,
    decrement,
    reset,
    doubleCount
  }
}

3.2 响应式数据处理

在组件中使用响应式数据:

// components/Counter.vue
import { defineComponent, ref, computed } from 'vue'
import { useCounter } from '@/composables/useCounter'

export default defineComponent({
  name: 'Counter',
  setup() {
    const { count, increment, decrement, reset, doubleCount } = useCounter(0)
    
    const message = computed(() => {
      if (count.value > 10) return 'Too many!'
      if (count.value < 0) return 'Too few!'
      return 'Just right!'
    })
    
    return {
      count,
      increment,
      decrement,
      reset,
      doubleCount,
      message
    }
  }
})

3.3 组件通信

使用 provide/inject 进行跨层级组件通信:

// composables/useTheme.ts
import { provide, inject, ref, Ref } from 'vue'

type Theme = 'light' | 'dark'

const THEME_KEY = Symbol('theme')

export function useTheme() {
  const theme: Ref<Theme> = ref('light')
  
  const toggleTheme = () => {
    theme.value = theme.value === 'light' ? 'dark' : 'light'
  }
  
  provide(THEME_KEY, {
    theme,
    toggleTheme
  })
  
  return { theme, toggleTheme }
}

export function useThemeContext() {
  const context = inject(THEME_KEY)
  
  if (!context) {
    throw new Error('useThemeContext must be used within a ThemeProvider')
  }
  
  return context
}

路由配置与导航

4.1 Vue Router 配置

安装 Vue Router:

npm install vue-router@4

配置路由:

// router/index.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import UserList from '@/views/UserList.vue'
import UserDetail from '@/views/UserDetail.vue'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/',
    name: 'Home',
    component: Home
  },
  {
    path: '/about',
    name: 'About',
    component: About
  },
  {
    path: '/users',
    name: 'Users',
    component: UserList
  },
  {
    path: '/users/:id',
    name: 'UserDetail',
    component: UserDetail,
    props: true
  }
]

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

export default router

4.2 路由守卫

// router/index.ts (续)
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { useAuthStore } from '@/store/auth'

const routes: Array<RouteRecordRaw> = [
  // ... 路由配置
  {
    path: '/admin',
    name: 'Admin',
    component: () => import('@/views/Admin.vue'),
    meta: { requiresAuth: true },
    beforeEnter: (to, from, next) => {
      const authStore = useAuthStore()
      
      if (!authStore.isAuthenticated) {
        next({ name: 'Home' })
      } else {
        next()
      }
    }
  }
]

// 全局前置守卫
router.beforeEach((to, from, next) => {
  // 添加加载状态
  document.body.classList.add('loading')
  
  next()
})

// 全局后置钩子
router.afterEach(() => {
  // 移除加载状态
  document.body.classList.remove('loading')
})

export default router

状态管理

5.1 Pinia 状态管理

安装 Pinia:

npm install pinia

创建 store:

// store/auth.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'

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

export const useAuthStore = defineStore('auth', () => {
  const user = ref<User | null>(null)
  const isAuthenticated = computed(() => !!user.value)
  
  const login = (userData: User) => {
    user.value = userData
  }
  
  const logout = () => {
    user.value = null
  }
  
  const updateUser = (userData: Partial<User>) => {
    if (user.value) {
      user.value = { ...user.value, ...userData }
    }
  }
  
  return {
    user,
    isAuthenticated,
    login,
    logout,
    updateUser
  }
})

5.2 复杂状态管理

// store/users.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import { User } from './auth'

export interface UserState {
  users: User[]
  loading: boolean
  error: string | null
  currentPage: number
  totalPages: number
}

export const useUserStore = defineStore('users', () => {
  const state = ref<UserState>({
    users: [],
    loading: false,
    error: null,
    currentPage: 1,
    totalPages: 1
  })
  
  const isLoading = computed(() => state.value.loading)
  const users = computed(() => state.value.users)
  const hasError = computed(() => !!state.value.error)
  
  const fetchUsers = async (page: number = 1) => {
    state.value.loading = true
    state.value.error = null
    
    try {
      // 模拟 API 调用
      const response = await fetch(`/api/users?page=${page}`)
      const data = await response.json()
      
      state.value.users = data.users
      state.value.currentPage = data.currentPage
      state.value.totalPages = data.totalPages
    } catch (error) {
      state.value.error = error instanceof Error ? error.message : 'Failed to fetch users'
    } finally {
      state.value.loading = false
    }
  }
  
  const addUser = (user: User) => {
    state.value.users.push(user)
  }
  
  const removeUser = (userId: number) => {
    state.value.users = state.value.users.filter(user => user.id !== userId)
  }
  
  return {
    ...state.value,
    isLoading,
    users,
    hasError,
    fetchUsers,
    addUser,
    removeUser
  }
})

API 服务与数据交互

6.1 API 服务封装

// services/api.ts
import axios, { AxiosInstance, AxiosRequestConfig } from 'axios'

class ApiService {
  private client: AxiosInstance
  
  constructor(baseURL: string) {
    this.client = axios.create({
      baseURL,
      timeout: 10000,
      headers: {
        'Content-Type': 'application/json'
      }
    })
    
    // 请求拦截器
    this.client.interceptors.request.use(
      (config) => {
        const token = localStorage.getItem('auth-token')
        if (token) {
          config.headers.Authorization = `Bearer ${token}`
        }
        return config
      },
      (error) => {
        return Promise.reject(error)
      }
    )
    
    // 响应拦截器
    this.client.interceptors.response.use(
      (response) => {
        return response.data
      },
      (error) => {
        if (error.response?.status === 401) {
          // 处理未授权
          localStorage.removeItem('auth-token')
          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, D>(url: string, data?: D, config?: AxiosRequestConfig): Promise<T> {
    return this.client.post<T>(url, data, config)
  }
  
  put<T, D>(url: string, data?: D, 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 apiService = new ApiService(import.meta.env.VITE_API_BASE_URL)

6.2 使用 API 服务

// composables/useUserApi.ts
import { ref, computed } from 'vue'
import { apiService } from '@/services/api'
import { User } from '@/store/auth'

export function useUserApi() {
  const users = ref<User[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  
  const fetchUsers = async () => {
    loading.value = true
    error.value = null
    
    try {
      const data = await apiService.get<User[]>('/users')
      users.value = data
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to fetch users'
    } finally {
      loading.value = false
    }
  }
  
  const createUser = async (userData: Omit<User, 'id'>) => {
    try {
      const newUser = await apiService.post<User, Omit<User, 'id'>>('/users', userData)
      users.value.push(newUser)
      return newUser
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to create user'
      throw err
    }
  }
  
  const updateUser = async (userId: number, userData: Partial<User>) => {
    try {
      const updatedUser = await apiService.put<User, Partial<User>>(`/users/${userId}`, userData)
      const index = users.value.findIndex(user => user.id === userId)
      if (index !== -1) {
        users.value[index] = updatedUser
      }
      return updatedUser
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to update user'
      throw err
    }
  }
  
  const deleteUser = async (userId: number) => {
    try {
      await apiService.delete(`/users/${userId}`)
      users.value = users.value.filter(user => user.id !== userId)
    } catch (err) {
      error.value = err instanceof Error ? err.message : 'Failed to delete user'
      throw err
    }
  }
  
  return {
    users: computed(() => users.value),
    loading: computed(() => loading.value),
    error: computed(() => error.value),
    fetchUsers,
    createUser,
    updateUser,
    deleteUser
  }
}

打包优化与性能调优

7.1 Vite 配置优化

// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { nodePolyfills } from 'vite-plugin-node-polyfills'
import { VitePWA } from 'vite-plugin-pwa'

export default defineConfig({
  plugins: [
    vue(),
    nodePolyfills(),
    VitePWA({
      registerType: 'autoUpdate',
      devOptions: {
        enabled: true
      },
      manifest: {
        name: 'My Vue App',
        short_name: 'VueApp',
        description: 'A Vue 3 + TypeScript + Vite application',
        theme_color: '#42b883',
        icons: [
          {
            src: '/pwa-192x192.png',
            sizes: '192x192',
            type: 'image/png'
          },
          {
            src: '/pwa-512x512.png',
            sizes: '512x512',
            type: 'image/png'
          }
        ]
      }
    })
  ],
  server: {
    port: 3000,
    host: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, '')
      }
    }
  },
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          vendor: ['vue', 'vue-router', 'pinia'],
          ui: ['element-plus', '@element-plus/icons-vue'],
          utils: ['axios', 'lodash-es']
        }
      }
    },
    terserOptions: {
      compress: {
        drop_console: true,
        drop_debugger: true
      }
    }
  },
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@import "@/styles/variables.scss";`
      }
    }
  }
})

7.2 代码分割与懒加载

// views/UserList.vue
import { defineComponent, onMounted } from 'vue'
import { useUserStore } from '@/store/users'
import { useUserApi } from '@/composables/useUserApi'

export default defineComponent({
  name: 'UserList',
  setup() {
    const userStore = useUserStore()
    const { users, loading, fetchUsers } = useUserApi()
    
    onMounted(() => {
      fetchUsers()
    })
    
    const handleRefresh = () => {
      fetchUsers()
    }
    
    return {
      users,
      loading,
      handleRefresh
    }
  }
})

7.3 缓存策略

// composables/useCache.ts
import { ref, watch } from 'vue'

export function useCache<T>(key: string, initialValue: T, ttl: number = 300000) {
  const cached = ref<T>(initialValue)
  const timestamp = ref<number>(Date.now())
  
  // 从缓存中获取数据
  const getCached = (): T | null => {
    const cachedData = localStorage.getItem(key)
    const cachedTime = localStorage.getItem(`${key}_timestamp`)
    
    if (cachedData && cachedTime) {
      const now = Date.now()
      if (now - parseInt(cachedTime) < ttl) {
        return JSON.parse(cachedData)
      } else {
        // 缓存过期,清除
        localStorage.removeItem(key)
        localStorage.removeItem(`${key}_timestamp`)
      }
    }
    
    return null
  }
  
  // 设置缓存
  const setCached = (data: T) => {
    localStorage.setItem(key, JSON.stringify(data))
    localStorage.setItem(`${key}_timestamp`, Date.now().toString())
    cached.value = data
  }
  
  // 初始化缓存
  const cachedData = getCached()
  if (cachedData) {
    cached.value = cachedData
  }
  
  // 监听数据变化并更新缓存
  watch(cached, (newVal) => {
    setCached(newVal)
  }, { deep: true })
  
  return {
    cached,
    getCached,
    setCached
  }
}

测试与调试

8.1 单元测试配置

安装测试依赖:

npm install -D @vue/test-utils jest @types/jest vue-jest

配置测试环境:

// jest.config.js
module.exports = {
  moduleFileExtensions: ['js', 'ts', 'vue'],
  transform: {
    '^.+\\.vue$': 'vue-jest',
    '^.+\\.ts$': 'ts-jest'
  },
  testMatch: ['**/__tests__/**/*.(spec|test).(ts|js)']
}

8.2 组件测试示例

// __tests__/components/UserCard.spec.ts
import { mount } from '@vue/test-utils'
import UserCard from '@/components/UserCard.vue'
import { User } from '@/store/auth'

describe('UserCard', () => {
  const mockUser: User = {
    id: 1,
    name: 'John Doe',
    email: 'john@example.com'
  }
  
  it('renders user information correctly', () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser,
        showEmail: true
      }
    })
    
    expect(wrapper.text()).toContain('John Doe')
    expect(wrapper.text()).toContain('john@example.com')
  })
  
  it('emits user-click event when clicked', async () => {
    const wrapper = mount(UserCard, {
      props: {
        user: mockUser
      }
    })
    
    await wrapper.find('.user-card').trigger('click')
    expect(wrapper.emitted('user-click')).toHaveLength(1)
    expect(wrapper.emitted('user-click')![0]).toEqual([1])
  })
})

部署与生产环境优化

9.1 生产环境构建

npm run build

9.2 环境变量管理

// env.d.ts
interface ImportMetaEnv {
  readonly VITE_API_BASE_URL: string
  readonly VITE_APP_NAME: string
  readonly VITE_APP_VERSION: string
}

interface ImportMeta {
  readonly env: ImportMetaEnv
}

9.3 部署配置

# .github/workflows/deploy.yml
name: Deploy
on:
  push:
    branches: [ main ]

jobs:
  deploy:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Setup Node.js
        uses: actions/setup-node@v2
        with:
          node-version: '16'
      - name: Install dependencies
        run: npm ci
      - name: Build
        run: npm run build
      - name: Deploy to Netlify
        uses: netlify/actions/cli@master
        with:
          args: deploy --dir=dist --prod
        env:
          NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}

最佳实践总结

10.1 代码组织原则

  1. 组件分离:将逻辑复杂的组件拆分为多个小组件
  2. 状态管理:合理使用 Pinia 进行状态管理
  3. 类型安全:充分利用 TypeScript 的类型系统
  4. 可维护性:保持代码简洁,遵循单一职责原则

10.2 性能优化建议

  1. 懒加载:对非关键资源使用懒加载
  2. 缓存策略:合理使用浏览器缓存和数据缓存
  3. 代码分割:利用 Vite 的自动代码分割功能
  4. 资源优化:压缩图片、字体等静态资源

10.3 开发体验提升

  1. TypeScript 支持:开启严格的类型检查
  2. 开发工具:使用 Vue DevTools 进行调试
  3. 错误处理:完善的错误边界和异常处理机制
  4. 测试覆盖:编写全面的单元测试和集成测试

结语

通过本文的详细介绍,我们已经掌握了使用 Vue 3、TypeScript 和 Vite 构建现代化前端应用的完整流程。从项目初始化到生产环境部署,从组件开发到状态管理,每一个环节都体现了现代前端开发的最佳实践。

这套技术栈不仅提供了强大的功能支持,还具有良好的开发体验和性能表现。随着前端技术的不断发展,我们应当持续关注新技术的演进,不断优化我们的开发流程,构建更加优秀、更加高效的前端应用。

记住,技术的选择应该基于项目需求和团队能力,选择最适合的工具和方法,才能真正提高开发效率和产品质量。希望本文能够为您的前端开发之旅提供有价值的参考和指导。

相关推荐
广告位招租

相似文章

    评论 (0)

    0/2000