Vue 3 与 Vite 现代前端开发完全教程
全面掌握 Vue 3 组合式 API 与 Vite 构建工具,从零搭建高性能前端应用
目录
- 一、概述与环境搭建
- 二、Vite 构建工具核心
- 三、Vue 3 组合式 API 深入
- 四、组件通信模式
- 五、Vue Router 路由管理
- 六、Pinia 状态管理
- 七、TypeScript 集成
- 八、单文件组件(SFC)高级特性
- 九、Teleport 与 Suspense
- 十、性能优化
- 十一、单元测试 Vitest
- 十二、总结与最佳实践
一、概述与环境搭建
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 由两部分组成:
- 开发服务器:利用浏览器原生 ES Module 支持,实现按需编译,冷启动极快
- 生产构建:使用 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
ref 和 reactive 是组合式 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 不支持基本类型 |
| 简单对象 | ref 或 reactive |
均可,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 编码规范要点
- 组合式函数命名:以
use开头,如useAuth、usePagination - 组件命名:使用 PascalCase,如
UserProfile.vue、ProductCard.vue - Props 类型:始终使用 TypeScript 接口定义 Props 类型
- Store 设计:按业务域划分 Store,保持单一职责
- 路由懒加载:所有页面组件都应使用动态
import() - 错误边界:使用
onErrorCaptured捕获子组件错误 - 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 现代前端开发的核心知识点。掌握这些内容后,你将具备构建企业级前端应用的能力。建议结合实际项目练习,深入理解每个概念的应用场景。