Vue 3与Vite现代前端开发完全教程

教程简介

全面讲解Vue 3组合式API与Vite构建工具的核心特性与实战开发,涵盖Composition API(ref/reactive/computed/watch)、组件通信模式、Vue Router路由管理、Pinia状态管理、Vite配置与插件开发、TypeScript集成、SFC高级特性、Teleport与Suspense、性能优化、单元测试(Vitest)等核心内容。

Vue 3 与 Vite 现代前端开发完全教程

全面掌握 Vue 3 组合式 API 与 Vite 构建工具,从零搭建高性能前端应用


目录


一、概述与环境搭建

1.1 Vue 3 简介

Vue 3 是 Vue.js 框架的第三个主要版本,于 2020 年 9 月正式发布。相较于 Vue 2,Vue 3 带来了以下核心改进:

  • Composition API:全新的组件逻辑组织方式,解决 Options API 在大型组件中的代码碎片化问题
  • 性能提升:虚拟 DOM 重写、静态树提升、补丁标记等优化,渲染性能提升约 1.3-2 倍
  • 更好的 TypeScript 支持:代码库完全用 TypeScript 重写
  • 更小的体积:Tree-shaking 支持,核心运行时仅约 10KB(gzip)
  • 新内置组件:Teleport、Suspense、Fragment 等

1.2 环境准备

确保已安装 Node.js(推荐 18+ 版本):

# 检查 Node.js 版本
node -v

# 使用 npm 创建 Vue 3 项目
npm create vue@latest

# 按提示选择配置:
# ✔ Add TypeScript? → Yes
# ✔ Add Vue Router? → Yes
# ✔ Add Pinia? → Yes
# ✔ Add Vitest? → Yes
# ✔ Add ESLint? → Yes

cd my-vue-app
npm install
npm run dev

1.3 项目结构一览

my-vue-app/
├── public/            # 静态资源(不经过构建处理)
├── src/
│   ├── assets/        # 需要构建处理的静态资源
│   ├── components/    # 通用组件
│   ├── composables/   # 组合式函数(自定义 Hooks)
│   ├── router/        # 路由配置
│   ├── stores/        # Pinia 状态管理
│   ├── views/         # 页面级组件
│   ├── App.vue        # 根组件
│   └── main.ts        # 应用入口
├── index.html         # HTML 入口模板
├── vite.config.ts     # Vite 配置
├── tsconfig.json      # TypeScript 配置
└── package.json       # 项目依赖与脚本

二、Vite 构建工具核心

2.1 Vite 的工作原理

Vite 由两部分组成:

  1. 开发服务器:利用浏览器原生 ES Module 支持,实现按需编译,冷启动极快
  2. 生产构建:使用 Rollup 打包,输出高度优化的静态资源

传统打包工具(如 Webpack)在开发时需要分析整个依赖图并打包,而 Vite 只在浏览器请求时才编译对应模块,这就是它开发体验极快的根本原因。

2.2 核心配置

// 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'),
      '@components': resolve(__dirname, 'src/components'),
    },
  },

  // 开发服务器配置
  server: {
    port: 3000,
    open: true,
    proxy: {
      '/api': {
        target: 'http://localhost:8080',
        changeOrigin: true,
        rewrite: (path) => path.replace(/^\/api/, ''),
      },
    },
  },

  // 生产构建配置
  build: {
    outDir: 'dist',
    sourcemap: false,
    rollupOptions: {
      output: {
        manualChunks: {
          vue: ['vue', 'vue-router', 'pinia'],
          vendor: ['axios', 'dayjs'],
        },
      },
    },
  },

  // CSS 预处理器
  css: {
    preprocessorOptions: {
      scss: {
        additionalData: `@use "@/styles/variables" as *;`,
      },
    },
  },
})

2.3 环境变量

Vite 使用 .env 文件管理环境变量,只有以 VITE_ 为前缀的变量才会暴露给客户端代码:

# .env.development
VITE_API_BASE_URL=http://localhost:8080/api
VITE_APP_TITLE=My Vue App (Dev)

# .env.production
VITE_API_BASE_URL=https://api.example.com
VITE_APP_TITLE=My Vue App

在代码中使用:

const apiUrl = import.meta.env.VITE_API_BASE_URL
const title = import.meta.env.VITE_APP_TITLE

2.4 Vite 插件开发

Vite 插件遵循 Rollup 插件接口,并扩展了 Vite 特有的钩子:

// plugins/console-remover.ts
import type { Plugin } from 'vite'

export function consoleRemover(): Plugin {
  return {
    name: 'console-remover',
    enforce: 'pre',
    transform(code, id) {
      if (id.includes('node_modules')) return null
      // 移除 console.log 语句
      const transformed = code.replace(/console\.(log|debug|info)\([^)]*\);?\s*/g, '')
      return {
        code: transformed,
        map: null,
      }
    },
  }
}

使用插件:

// vite.config.ts
import { consoleRemover } from './plugins/console-remover'

export default defineConfig({
  plugins: [vue(), consoleRemover()],
})

三、Vue 3 组合式 API 深入

3.1 ref 与 reactive

refreactive 是组合式 API 中创建响应式数据的两个核心 API:

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

// ref:适用于基本类型,也可用于对象
const count = ref(0)
const message = ref('Hello Vue 3')

// reactive:适用于对象和数组
const state = reactive({
  name: '张三',
  age: 25,
  hobbies: ['编程', '阅读'],
})

// 访问 ref 的值需要 .value
console.log(count.value) // 0

// reactive 直接访问属性
console.log(state.name) // 张三

// 在模板中 ref 会自动解包,无需 .value
// <template>
//   <p>{{ count }}</p>
//   <p>{{ state.name }}</p>
// </template>
</script>

ref 与 reactive 的选择原则

场景 推荐 原因
基本类型 ref reactive 不支持基本类型
简单对象 refreactive 均可,ref 更统一
复杂嵌套对象 reactive 无需到处写 .value
需要整体替换 ref reactive 替换会丢失响应性

3.2 computed 计算属性

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

const firstName = ref('张')
const lastName = ref('三')

// 只读计算属性
const fullName = computed(() => `${firstName.value}${lastName.value}`)

// 可写计算属性
const fullNameWritable = computed({
  get: () => `${firstName.value}${lastName.value}`,
  set: (val: string) => {
    firstName.value = val.charAt(0)
    lastName.value = val.slice(1)
  },
})

// 缓存优化:只在依赖变化时重新计算
const expensiveResult = computed(() => {
  console.log('计算执行') // 仅在 firstName 或 lastName 变化时打印
  return firstName.value + lastName.value + ' - 已处理'
})
</script>

3.3 watch 与 watchEffect

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

const keyword = ref('')
const page = ref(1)

// watch:明确指定要监听的源
watch(keyword, (newVal, oldVal) => {
  console.log(`搜索词从 "${oldVal}" 变为 "${newVal}"`)
  page.value = 1 // 关键词变化时重置页码
})

// 监听多个源
watch([keyword, page], ([newKeyword, newPage], [oldKeyword, oldPage]) => {
  console.log(`搜索: ${newKeyword}, 页码: ${newPage}`)
  fetchData(newKeyword, newPage)
})

// 深度监听对象
const user = ref({ name: '张三', address: { city: '北京' } })
watch(user, (newVal) => {
  console.log('用户信息变化', newVal)
}, { deep: true })

// watchEffect:自动追踪依赖,立即执行
watchEffect(() => {
  // 自动追踪内部用到的所有响应式数据
  console.log(`当前搜索: ${keyword.value}, 页码: ${page.value}`)
})

// 清理副作用
watch(keyword, async (newVal, oldVal, onCleanup) => {
  let cancelled = false
  onCleanup(() => { cancelled = true }) // 关键词快速变化时取消前一次请求
  
  const result = await fetch(`/api/search?q=${newVal}`)
  if (!cancelled) {
    // 处理结果
  }
})
</script>

3.4 生命周期钩子

<script setup lang="ts">
import {
  onBeforeMount,
  onMounted,
  onBeforeUpdate,
  onUpdated,
  onBeforeUnmount,
  onUnmounted,
  onActivated,
  onDeactivated,
  onErrorCaptured,
} from 'vue'

onMounted(() => {
  console.log('组件挂载完成,可以访问 DOM')
  // 适合:发起请求、添加事件监听、初始化第三方库
})

onBeforeUnmount(() => {
  console.log('组件即将卸载')
  // 适合:清理定时器、移除事件监听、取消订阅
})

onErrorCaptured((err, instance, info) => {
  console.error('捕获到子组件错误:', err)
  return false // 阻止错误继续向上传播
})
</script>

四、组件通信模式

4.1 Props 与 Emits(父子组件)

<!-- ParentComponent.vue -->
<script setup lang="ts">
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'

const parentMsg = ref('来自父组件的消息')
const handleChildEvent = (data: string) => {
  console.log('收到子组件事件:', data)
}
</script>

<template>
  <ChildComponent
    :message="parentMsg"
    :count="42"
    @update="handleChildEvent"
  />
</template>
<!-- ChildComponent.vue -->
<script setup lang="ts">
// 使用 defineProps 定义 props(支持类型声明)
const props = defineProps<{
  message: string
  count?: number
}>()

// 使用 defineEmits 定义事件
const emit = defineEmits<{
  (e: 'update', data: string): void
  (e: 'delete', id: number): void
}>()

const handleClick = () => {
  emit('update', '子组件的数据')
}
</script>

<template>
  <div>
    <p>{{ message }} - {{ count }}</p>
    <button @click="handleClick">发送事件</button>
  </div>
</template>

4.2 Provide / Inject(跨层级通信)

<!-- GrandParent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'

const theme = ref<'light' | 'dark'>('light')
const toggleTheme = () => {
  theme.value = theme.value === 'light' ? 'dark' : 'light'
}

// provide 向所有后代组件提供数据
provide('theme', theme)
provide('toggleTheme', toggleTheme)
</script>
<!-- DeepChild.vue -->
<script setup lang="ts">
import { inject } from 'vue'

// inject 注入祖先组件提供的数据
const theme = inject<string>('theme', 'light')
const toggleTheme = inject<() => void>('toggleTheme', () => {})
</script>

<template>
  <div :class="`theme-${theme}`">
    <button @click="toggleTheme">切换主题</button>
  </div>
</template>

4.3 全局事件总线(mitt)

对于非父子关系的兄弟组件通信,推荐使用轻量级事件库 mitt

// utils/eventBus.ts
import mitt from 'mitt'

type Events = {
  'user:login': { userId: string; token: string }
  'user:logout': void
  'notification': { type: string; message: string }
}

export const eventBus = mitt<Events>()
<!-- ComponentA.vue -->
<script setup lang="ts">
import { eventBus } from '@/utils/eventBus'

const login = () => {
  eventBus.emit('user:login', { userId: '001', token: 'abc123' })
}
</script>
<!-- ComponentB.vue -->
<script setup lang="ts">
import { onMounted, onUnmounted } from 'vue'
import { eventBus } from '@/utils/eventBus'

onMounted(() => {
  eventBus.on('user:login', (data) => {
    console.log('用户已登录:', data.userId)
  })
})

onUnmounted(() => {
  eventBus.off('user:login')
})
</script>

五、Vue Router 路由管理

5.1 路由配置

// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    name: 'Home',
    component: () => import('@/views/HomeView.vue'),
    meta: { title: '首页', requiresAuth: false },
  },
  {
    path: '/user/:id',
    name: 'UserProfile',
    component: () => import('@/views/UserProfile.vue'),
    meta: { title: '用户详情', requiresAuth: true },
    props: true, // 将路由参数作为 props 传递
    children: [
      {
        path: 'posts',
        name: 'UserPosts',
        component: () => import('@/views/UserPosts.vue'),
      },
      {
        path: 'settings',
        name: 'UserSettings',
        component: () => import('@/views/UserSettings.vue'),
      },
    ],
  },
  {
    path: '/:pathMatch(.*)*',
    name: 'NotFound',
    component: () => import('@/views/NotFound.vue'),
  },
]

const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    if (savedPosition) return savedPosition
    return { top: 0 }
  },
})

// 全局前置守卫
router.beforeEach((to, from) => {
  const isAuthenticated = !!localStorage.getItem('token')

  if (to.meta.requiresAuth && !isAuthenticated) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// 全局后置钩子
router.afterEach((to) => {
  document.title = `${to.meta.title} - My App`
})

export default router

5.2 路由组合式用法

<script setup lang="ts">
import { useRouter, useRoute } from 'vue-router'

const router = useRouter()
const route = useRoute()

// 获取路由参数
const userId = route.params.id as string
const query = route.query

// 编程式导航
const goToUser = (id: string) => {
  router.push({ name: 'UserProfile', params: { id } })
}

const goBack = () => {
  router.back()
}

// 响应路由参数变化
watch(() => route.params.id, (newId) => {
  if (newId) fetchUser(newId as string)
})
</script>

六、Pinia 状态管理

6.1 Store 定义

Pinia 是 Vue 官方推荐的状态管理库,相比 Vuex 更简洁、TypeScript 友好:

// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
import type { User } from '@/types'

export const useUserStore = defineStore('user', () => {
  // state
  const currentUser = ref<User | null>(null)
  const token = ref<string>('')
  const loading = ref(false)

  // getters
  const isLoggedIn = computed(() => !!token.value)
  const userName = computed(() => currentUser.value?.name ?? '游客')

  // actions
  async function login(username: string, password: string) {
    loading.value = true
    try {
      const res = await fetch('/api/login', {
        method: 'POST',
        body: JSON.stringify({ username, password }),
        headers: { 'Content-Type': 'application/json' },
      })
      const data = await res.json()
      token.value = data.token
      currentUser.value = data.user
      localStorage.setItem('token', data.token)
    } catch (error) {
      console.error('登录失败:', error)
      throw error
    } finally {
      loading.value = false
    }
  }

  function logout() {
    token.value = ''
    currentUser.value = null
    localStorage.removeItem('token')
  }

  return {
    currentUser,
    token,
    loading,
    isLoggedIn,
    userName,
    login,
    logout,
  }
})

6.2 Store 使用与持久化

<script setup lang="ts">
import { useUserStore } from '@/stores/user'
import { storeToRefs } from 'pinia'

const userStore = useUserStore()

// 使用 storeToRefs 保持响应性(解构 state/getters 时必须)
const { isLoggedIn, userName, loading } = storeToRefs(userStore)

// actions 直接解构即可
const { login, logout } = userStore
</script>

<template>
  <div v-if="isLoggedIn">
    <span>欢迎,{{ userName }}</span>
    <button @click="logout">退出</button>
  </div>
  <div v-else>
    <button @click="login('admin', '123456')" :disabled="loading">
      {{ loading ? '登录中...' : '登录' }}
    </button>
  </div>
</template>

安装持久化插件:

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

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

在 Store 中启用持久化:

export const useUserStore = defineStore('user', () => {
  // ...state, getters, actions
}, {
  persist: {
    storage: localStorage,
    pick: ['token'], // 只持久化 token 字段
  },
})

七、TypeScript 集成

7.1 组件中的 TypeScript

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

// 接口定义
interface Product {
  id: number
  name: string
  price: number
  tags: string[]
}

// ref 类型推断与显式声明
const products = ref<Product[]>([])
const selectedId = ref<number | null>(null)

// reactive 类型推断
const form = reactive({
  keyword: '',
  category: '',
  page: 1,
  pageSize: 10,
})

// 函数类型注解
const filterProducts = (keyword: string, minPrice?: number): Product[] => {
  return products.value.filter((p) => {
    const matchKeyword = p.name.includes(keyword)
    const matchPrice = minPrice ? p.price >= minPrice : true
    return matchKeyword && matchPrice
  })
}

// 泛型函数
async function fetchData<T>(url: string): Promise<T> {
  const res = await fetch(url)
  return res.json() as Promise<T>
}

// 使用
const loadProducts = async () => {
  products.value = await fetchData<Product[]>('/api/products')
}
</script>

7.2 组件 Props 类型

<script setup lang="ts">
// 方式一:类型字面量
const props = defineProps<{
  title: string
  items: string[]
  visible?: boolean
  onClose: () => void
}>()

// 方式二:带默认值(使用 withDefaults)
const props2 = withDefaults(
  defineProps<{
    title: string
    items?: string[]
    visible?: boolean
  }>(),
  {
    items: () => [],
    visible: true,
  }
)
</script>

八、单文件组件(SFC)高级特性

8.1 <script setup> 语法糖

<script setup> 是 Vue 3.2 引入的编译时语法糖,让组合式 API 更简洁:

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

// 所有顶层变量、函数、import 均自动暴露给模板
const count = ref(0)
const increment = () => count.value++
</script>

<template>
  <MyComponent :count="count" @click="increment" />
</template>

8.2 CSS 特性

<!-- 作用域样式 -->
<style scoped>
.card {
  padding: 16px;
  border-radius: 8px;
}
</style>

<!-- 深度选择器:影响子组件样式 -->
<style scoped>
.parent :deep(.child-component .inner) {
  color: red;
}
</style>

<!-- 全局样式 -->
<style global>
body {
  margin: 0;
  font-family: system-ui;
}
</style>

<!-- CSS Modules -->
<style module>
.container {
  max-width: 1200px;
}
</style>

<script setup lang="ts">
const style = useCssModule()
// 使用::class="style.container"
</script>

8.3 v-model 与自定义指令

<script setup lang="ts">
// 自定义 v-model 修饰符
const props = defineProps<{
  modelValue: string
  modelModifiers?: { capitalize?: boolean }
}>()

const emit = defineEmits<{
  (e: 'update:modelValue', value: string): void
}>()

const onInput = (e: Event) => {
  let value = (e.target as HTMLInputElement).value
  if (props.modelModifiers?.capitalize) {
    value = value.charAt(0).toUpperCase() + value.slice(1)
  }
  emit('update:modelValue', value)
}
</script>

九、Teleport 与 Suspense

9.1 Teleport 传送门

Teleport 允许将组件的 DOM 渲染到指定的目标位置,常用于模态框、通知等:

<script setup lang="ts">
import { ref } from 'vue'
const showModal = ref(false)
</script>

<template>
  <button @click="showModal = true">打开弹窗</button>
  
  <!-- 将模态框渲染到 body 下 -->
  <Teleport to="body">
    <div v-if="showModal" class="modal-overlay" @click.self="showModal = false">
      <div class="modal-content">
        <h2>这是一个模态框</h2>
        <p>尽管 Teleport 在组件内部定义,但 DOM 实际渲染在 body 下</p>
        <button @click="showModal = false">关闭</button>
      </div>
    </div>
  </Teleport>
</template>

9.2 Suspense 异步组件

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

const AsyncDashboard = defineAsyncComponent(() =>
  import('./AsyncDashboard.vue')
)
</script>

<template>
  <Suspense>
    <!-- 主内容(异步组件) -->
    <template #default>
      <AsyncDashboard />
    </template>
    
    <!-- 加载状态 -->
    <template #fallback>
      <div class="loading-spinner">
        <span>加载中...</span>
      </div>
    </template>
  </Suspense>
</template>

异步组件中使用 await

<!-- AsyncDashboard.vue -->
<script setup lang="ts">
// 顶层 await 会让组件成为一个异步依赖
const res = await fetch('/api/dashboard')
const data = await res.json()
</script>

<template>
  <div>{{ data.title }}</div>
</template>

十、性能优化

10.1 组件懒加载

// 路由级懒加载(已默认启用)
const routes = [
  {
    path: '/admin',
    component: () => import('@/views/AdminView.vue'),
  },
]

// 组件级懒加载
import { defineAsyncComponent } from 'vue'

const HeavyChart = defineAsyncComponent({
  loader: () => import('@/components/HeavyChart.vue'),
  loadingComponent: LoadingSpinner,
  delay: 200, // 延迟显示 loading(避免闪烁)
  errorComponent: ErrorDisplay,
  timeout: 10000,
})

10.2 虚拟滚动

处理大量列表数据时,使用虚拟滚动只渲染可见区域:

npm install @tanstack/vue-virtual
<script setup lang="ts">
import { ref } from 'vue'
import { useVirtualizer } from '@tanstack/vue-virtual'

const parentRef = ref<HTMLElement | null>(null)
const items = ref(Array.from({ length: 10000 }, (_, i) => `Item ${i}`))

const virtualizer = useVirtualizer({
  count: items.value.length,
  getScrollElement: () => parentRef.value,
  estimateSize: () => 35,
  overscan: 5,
})
</script>

<template>
  <div ref="parentRef" style="height: 400px; overflow: auto;">
    <div :style="{ height: `${virtualizer.getTotalSize()}px`, position: 'relative' }">
      <div
        v-for="row in virtualizer.getVirtualItems()"
        :key="row.key"
        :style="{
          position: 'absolute',
          top: `${row.start}px`,
          height: `${row.size}px`,
          width: '100%',
        }"
      >
        {{ items[row.index] }}
      </div>
    </div>
  </div>
</template>

10.3 其他优化手段

<script setup lang="ts">
import { shallowRef, markRaw, vMemo } from 'vue'

// shallowRef:只追踪 .value 变化,不深层响应
const bigList = shallowRef([...data])

// markRaw:标记对象永不转为响应式
const thirdPartyLib = markRaw(new SomeLibrary())

// v-once:静态内容只渲染一次
// v-memo:条件缓存(Vue 3.2+)
</script>

<template>
  <!-- v-memo:只在依赖变化时重新渲染 -->
  <div v-for="item in list" :key="item.id" v-memo="[item.id === selected]">
    <p :class="{ selected: item.id === selected }">{{ item.name }}</p>
  </div>

  <!-- 静态内容缓存 -->
  <header v-once>
    <h1>这个标题只渲染一次</h1>
  </header>
</template>

十一、单元测试 Vitest

11.1 基础配置

Vitest 是与 Vite 深度集成的测试框架,配置简单、速度快:

// vitest.config.ts
import { defineConfig } from 'vitest/config'
import vue from '@vitejs/plugin-vue'

export default defineConfig({
  plugins: [vue()],
  test: {
    environment: 'jsdom',
    globals: true,
    include: ['**/*.{test,spec}.{js,ts}'],
    coverage: {
      provider: 'v8',
      reporter: ['text', 'json', 'html'],
    },
  },
})

11.2 组件测试

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

describe('Counter 组件', () => {
  it('渲染初始值', () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 5 },
    })
    expect(wrapper.text()).toContain('5')
  })

  it('点击按钮增加计数', async () => {
    const wrapper = mount(Counter, {
      props: { initialCount: 0 },
    })
    
    await wrapper.find('button.increment').trigger('click')
    expect(wrapper.text()).toContain('1')
    
    await wrapper.find('button.increment').trigger('click')
    expect(wrapper.text()).toContain('2')
  })

  it('触发自定义事件', async () => {
    const wrapper = mount(Counter)
    
    await wrapper.find('button.increment').trigger('click')
    
    expect(wrapper.emitted('change')).toHaveLength(1)
    expect(wrapper.emitted('change')![0]).toEqual([1])
  })
})

11.3 Store 测试

// stores/user.test.ts
import { describe, it, expect, beforeEach, vi } from 'vitest'
import { setActivePinia, createPinia } from 'pinia'
import { useUserStore } from './user'

describe('User Store', () => {
  beforeEach(() => {
    setActivePinia(createPinia())
  })

  it('初始状态', () => {
    const store = useUserStore()
    expect(store.isLoggedIn).toBe(false)
    expect(store.currentUser).toBeNull()
  })

  it('登录成功', async () => {
    // Mock fetch
    vi.stubGlobal('fetch', vi.fn().mockResolvedValue({
      json: () => Promise.resolve({
        token: 'test-token',
        user: { name: '张三' },
      }),
    }))

    const store = useUserStore()
    await store.login('admin', '123456')

    expect(store.isLoggedIn).toBe(true)
    expect(store.userName).toBe('张三')
  })

  it('登出', async () => {
    const store = useUserStore()
    store.logout()

    expect(store.isLoggedIn).toBe(false)
    expect(store.currentUser).toBeNull()
  })
})

运行测试:

# 运行所有测试
npx vitest

# 运行并监听变化
npx vitest --watch

# 生成覆盖率报告
npx vitest --coverage

# 运行 UI 界面
npx vitest --ui

十二、总结与最佳实践

12.1 项目组织建议

src/
├── components/          # 按功能分组
│   ├── common/          # 通用组件(Button, Modal, Toast)
│   ├── layout/          # 布局组件(Header, Sidebar, Footer)
│   └── features/        # 业务组件
│       ├── user/
│       └── product/
├── composables/         # 组合式函数
│   ├── useAuth.ts
│   ├── usePagination.ts
│   └── useRequest.ts
├── stores/              # Pinia 状态
├── utils/               # 工具函数
├── types/               # TypeScript 类型定义
├── constants/           # 常量
└── styles/              # 全局样式

12.2 编码规范要点

  1. 组合式函数命名:以 use 开头,如 useAuthusePagination
  2. 组件命名:使用 PascalCase,如 UserProfile.vueProductCard.vue
  3. Props 类型:始终使用 TypeScript 接口定义 Props 类型
  4. Store 设计:按业务域划分 Store,保持单一职责
  5. 路由懒加载:所有页面组件都应使用动态 import()
  6. 错误边界:使用 onErrorCaptured 捕获子组件错误
  7. TypeScript:开启严格模式,充分利用类型推断

12.3 常见陷阱

// ❌ 错误:解构 reactive 会丢失响应性
const state = reactive({ count: 0 })
const { count } = state // count 不再是响应式的

// ✅ 正确:使用 toRefs
const { count } = toRefs(state)

// ❌ 错误:解构 store 的 state 会丢失响应性
const { currentUser } = useUserStore()

// ✅ 正确:使用 storeToRefs
const { currentUser } = storeToRefs(useUserStore())

// ❌ 错误:在 reactive 中使用 ref 未自动解包的情况
const state = reactive({ count: ref(0) })
console.log(state.count) // 需要 state.count.value(在 reactive 顶层才自动解包)

本教程涵盖了 Vue 3 与 Vite 现代前端开发的核心知识点。掌握这些内容后,你将具备构建企业级前端应用的能力。建议结合实际项目练习,深入理解每个概念的应用场景。

内容声明

本文内容为AI技术学习教程,仅供学习参考。如涉及技术问题,欢迎通过 xurj005@163.com 与我们交流。

目录