本文最后更新于 1 分钟前,文中所描述的信息可能已发生改变。
Vue.js 是当今最流行的前端框架之一,特别是随着 Vue 3 的发布,它带来了更好的性能、更小的体积和更好的 TypeScript 支持。本文将深入探讨 Vue.js 3 的开发实践,包括组件设计、状态管理、性能优化和最佳实践。
Vue 3 的新特性
Vue 3 相比 Vue 2 有许多重大改进:
- Composition API:提供更好的代码组织和逻辑复用
- 更好的 TypeScript 支持:从底层重写,提供更好的类型推断
- 更小的包体积:核心代码体积减少约 41%
- 更快的渲染性能:虚拟 DOM 重新实现,渲染速度提升最高可达 100%
- Fragments:允许组件返回多个根节点
- Teleport:可以将组件的一部分传送到 DOM 的其他部分
- Suspense:处理异步组件加载状态
环境搭建
使用 Vite 创建项目
Vite 是 Vue 团队开发的下一代前端构建工具,它显著提升了开发体验:
# 安装
npm create vite@latest my-vue-app -- --template vue
# 或使用 yarn
yarn create vite my-vue-app --template vue
# 进入项目目录
cd my-vue-app
# 安装依赖
npm install
# 或
yarn
# 启动开发服务器
npm run dev
# 或
yarn dev
项目目录结构
推荐的目录结构:
src/
├── assets/ # 静态资源
├── components/ # 通用组件
│ └── common/ # 公共UI组件
│ └── layout/ # 布局组件
├── composables/ # 组合式函数
├── views/ # 页面组件
├── router/ # 路由配置
├── stores/ # Pinia 状态存储
├── services/ # API 服务
├── utils/ # 工具函数
├── App.vue # 根组件
└── main.js # 入口文件
Composition API 实践
基本用法
在 Vue 3 中,Composition API 是组织组件逻辑的新方式:
<script setup>
import { ref, computed, onMounted } from 'vue'
// 响应式状态
const count = ref(0)
// 计算属性
const doubleCount = computed(() => count.value * 2)
// 方法
function increment() {
count.value++
}
// 生命周期钩子
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">Increment</button>
</div>
</template>
逻辑复用与组合式函数
组合式函数(Composables)是 Vue 3 中复用逻辑的主要方式:
// useCounter.js
import { ref, computed } from 'vue'
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value--
}
return {
count,
doubleCount,
increment,
decrement
}
}
在组件中使用:
<script setup>
import { useCounter } from '@/composables/useCounter'
const { count, doubleCount, increment, decrement } = useCounter(10)
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="decrement">-</button>
</div>
</template>
响应式工具
Vue 3 提供了丰富的响应式 API:
<script setup>
import { ref, reactive, computed, watch, watchEffect } from 'vue'
// ref 用于基本类型
const message = ref('Hello')
// reactive 用于对象
const user = reactive({
name: 'John',
age: 30
})
// computed 创建计算属性
const greeting = computed(() => `${message.value}, ${user.name}!`)
// watch 监听特定数据变化
watch(message, (newValue, oldValue) => {
console.log(`message changed from "${oldValue}" to "${newValue}"`)
}, { immediate: true })
// watchEffect 自动收集依赖并监听
watchEffect(() => {
console.log(`User ${user.name} is ${user.age} years old`)
})
// 更新方法
function updateUser() {
user.name = 'Jane'
user.age = 25
message.value = 'Welcome'
}
</script>
<template>
<div>
<p>{{ greeting }}</p>
<button @click="updateUser">Update User</button>
</div>
</template>
组件设计模式
Props 与事件
父子组件通信的基本方式:
<!-- 子组件: ChildComponent.vue -->
<script setup>
defineProps({
title: {
type: String,
required: true
},
count: {
type: Number,
default: 0
}
})
const emit = defineEmits(['increment', 'update:count'])
function handleClick() {
emit('increment')
emit('update:count', props.count + 1) // 用于 v-model 绑定
}
</script>
<template>
<div>
<h2>{{ title }}</h2>
<p>Count: {{ count }}</p>
<button @click="handleClick">Increment</button>
</div>
</template>
父组件使用:
<!-- 父组件 -->
<script setup>
import { ref } from 'vue'
import ChildComponent from './ChildComponent.vue'
const counter = ref(0)
function handleIncrement() {
console.log('Incremented!')
}
</script>
<template>
<div>
<ChildComponent
title="My Counter"
:count="counter"
@increment="handleIncrement"
@update:count="counter = $event"
/>
<!-- 或使用 v-model 简化 -->
<ChildComponent
title="My Counter with v-model"
v-model:count="counter"
@increment="handleIncrement"
/>
</div>
</template>
插槽(Slots)
使用插槽实现内容分发:
<!-- 基础插槽:CardComponent.vue -->
<template>
<div class="card">
<div class="card-header">
<slot name="header">默认标题</slot>
</div>
<div class="card-body">
<slot>默认内容</slot>
</div>
<div class="card-footer">
<slot name="footer"></slot>
</div>
</div>
</template>
使用组件:
<template>
<CardComponent>
<template #header>
<h3>自定义标题</h3>
</template>
<p>这是主要内容</p>
<template #footer>
<button>确认</button>
<button>取消</button>
</template>
</CardComponent>
</template>
动态组件
使用 <component>
动态切换组件:
<script setup>
import { ref, shallowRef } from 'vue'
import TabA from './TabA.vue'
import TabB from './TabB.vue'
import TabC from './TabC.vue'
// 使用 shallowRef 可提高性能,因为组件对象不需要深层响应
const currentTab = shallowRef(TabA)
const tabs = {
TabA,
TabB,
TabC
}
function switchTab(tab) {
currentTab.value = tabs[tab]
}
</script>
<template>
<div>
<button @click="switchTab('TabA')">Tab A</button>
<button @click="switchTab('TabB')">Tab B</button>
<button @click="switchTab('TabC')">Tab C</button>
<component :is="currentTab" />
</div>
</template>
状态管理
Pinia
Pinia 是 Vue 官方推荐的状态管理库,替代了 Vuex:
安装:
npm install pinia
# 或
yarn add pinia
配置:
// main.js
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'
const app = createApp(App)
app.use(createPinia())
app.mount('#app')
创建 store:
// stores/counter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// 状态
state: () => ({
count: 0,
name: 'Counter'
}),
// 类似计算属性
getters: {
doubleCount: (state) => state.count * 2,
// 使用this访问其他getter
formattedCounter() {
return `${this.name}: ${this.count}`
}
},
// 修改状态的方法
actions: {
increment() {
this.count++
},
async fetchInitialCount() {
const data = await fetch('/api/counter')
const result = await data.json()
this.count = result.count
}
}
})
使用 Composition API 风格定义 store:
// stores/counter.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useCounterStore = defineStore('counter', () => {
// 状态
const count = ref(0)
const name = ref('Counter')
// getters
const doubleCount = computed(() => count.value * 2)
// actions
function increment() {
count.value++
}
async function fetchInitialCount() {
const data = await fetch('/api/counter')
const result = await data.json()
count.value = result.count
}
return { count, name, doubleCount, increment, fetchInitialCount }
})
在组件中使用:
<script setup>
import { useCounterStore } from '@/stores/counter'
import { storeToRefs } from 'pinia'
const store = useCounterStore()
// 解构时保持响应性
const { count, doubleCount } = storeToRefs(store)
// 方法可以直接解构
const { increment } = store
</script>
<template>
<div>
<p>Count: {{ count }}</p>
<p>Double: {{ doubleCount }}</p>
<button @click="increment">+</button>
<button @click="store.count++">+ (直接修改)</button>
</div>
</template>
路由管理
Vue Router
Vue Router 是官方的路由管理器:
安装:
npm install vue-router@4
# 或
yarn add vue-router@4
基本配置:
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
import NotFound from '@/views/NotFound.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/users/:id',
name: 'UserDetail',
// 路由级代码分割
component: () => import('@/views/UserDetail.vue'),
// 路由元信息
meta: {
requiresAuth: true
}
},
// 404 路由
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: NotFound
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 全局前置守卫
router.beforeEach((to, from) => {
// 检查是否需要认证
if (to.meta.requiresAuth && !isAuthenticated()) {
// 重定向到登录页
return { name: 'Login', query: { redirect: to.fullPath } }
}
})
export default router
在 main.js
中注册:
import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
const app = createApp(App)
app.use(router)
app.mount('#app')
使用路由:
<script setup>
import { useRouter, useRoute } from 'vue-router'
const router = useRouter()
const route = useRoute()
// 获取当前路由参数
const userId = route.params.id
// 编程式导航
function navigateToHome() {
router.push('/')
}
function goToUser(id) {
router.push({
name: 'UserDetail',
params: { id }
})
}
</script>
<template>
<div>
<h1>用户详情</h1>
<p>用户ID: {{ userId }}</p>
<button @click="navigateToHome">返回首页</button>
<button @click="goToUser(123)">查看用户123</button>
<!-- 声明式导航 -->
<router-link to="/">首页</router-link>
<router-link :to="{ name: 'About' }">关于</router-link>
<!-- 嵌套路由出口 -->
<router-view />
</div>
</template>
性能优化
虚拟列表
处理大量数据时,使用虚拟列表:
<script setup>
import { ref } from 'vue'
// 示例数据
const items = ref(Array.from({ length: 10000 }, (_, i) => ({
id: i,
text: `Item ${i}`
})))
// 可视区域配置
const visibleItems = ref([])
const itemHeight = 40
const containerHeight = 400
const visibleCount = Math.ceil(containerHeight / itemHeight) + 2 // 额外渲染一些项防止滚动时出现空白
let startIndex = 0
// 计算可视项
function updateVisibleItems(scrollTop = 0) {
startIndex = Math.floor(scrollTop / itemHeight)
const start = Math.max(0, startIndex)
const end = Math.min(items.value.length, start + visibleCount)
visibleItems.value = items.value.slice(start, end).map(item => ({
...item,
style: {
position: 'absolute',
top: `${item.id * itemHeight}px`,
height: `${itemHeight}px`
}
}))
}
// 初始化
updateVisibleItems()
// 处理滚动
function handleScroll(e) {
updateVisibleItems(e.target.scrollTop)
}
</script>
<template>
<div
class="virtual-list"
@scroll="handleScroll"
:style="{
height: `${containerHeight}px`,
overflow: 'auto',
position: 'relative'
}"
>
<div :style="{ height: `${items.length * itemHeight}px` }">
<div
v-for="item in visibleItems"
:key="item.id"
:style="item.style"
class="list-item"
>
{{ item.text }}
</div>
</div>
</div>
</template>
<style scoped>
.virtual-list {
border: 1px solid #ccc;
}
.list-item {
padding: 10px;
border-bottom: 1px solid #eee;
box-sizing: border-box;
width: 100%;
}
</style>
组件懒加载
使用 Vue Router 的动态导入实现组件懒加载:
// 懒加载路由组件
const routes = [
{
path: '/dashboard',
component: () => import('./views/Dashboard.vue')
}
]
使用 Suspense 处理异步组件
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script setup>
import { defineAsyncComponent } from 'vue'
const AsyncComponent = defineAsyncComponent(() =>
import('./components/HeavyComponent.vue')
)
</script>
使用 v-memo 避免不必要的重渲染
<template>
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.active]">
<!-- 只有当 item.id 或 item.active 变化时才会重新渲染 -->
<span>{{ item.name }}</span>
<span>{{ item.description }}</span>
</div>
</template>
测试
使用 Vitest 进行单元测试
安装:
npm install -D vitest @vue/test-utils happy-dom
配置 vite.config.js
:
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
test: {
environment: 'happy-dom'
}
})
编写测试:
// counter.spec.js
import { mount } from '@vue/test-utils'
import { test, expect, describe } from 'vitest'
import Counter from '../components/Counter.vue'
describe('Counter.vue', () => {
test('increments count when button is clicked', async () => {
const wrapper = mount(Counter, {
props: {
initialCount: 5
}
})
// 检查初始渲染
expect(wrapper.text()).toContain('Count: 5')
// 模拟按钮点击
await wrapper.find('button').trigger('click')
// 检查更新后的状态
expect(wrapper.text()).toContain('Count: 6')
})
})
最佳实践
- 使用 TypeScript:提供更好的开发体验和类型安全
<script setup lang="ts">
import { ref, computed } from 'vue'
interface User {
id: number
name: string
email: string
}
const user = ref<User | null>(null)
const loading = ref(false)
const error = ref<Error | null>(null)
const fullName = computed(() =>
user.value ? `${user.value.name} (${user.value.id})` : 'Guest'
)
async function fetchUser(id: number): Promise<void> {
loading.value = true
error.value = null
try {
const response = await fetch(`/api/users/${id}`)
if (!response.ok) {
throw new Error('Failed to fetch user')
}
user.value = await response.json()
} catch (err) {
error.value = err as Error
} finally {
loading.value = false
}
}
</script>
使用组合式函数抽取和复用逻辑
使用
<script setup>
简化组件使用 Pinia 进行状态管理
按需加载组件减小包体积
正确使用
computed
与watch
:computed
适用于派生状态watch
适用于响应状态变化的副作用
避免滥用
nextTick
:通常不需要手动调用使用 CSS 自定义属性实现主题:
<style>
:root {
--primary-color: #42b883;
--secondary-color: #35495e;
--text-color: #333;
--background-color: #fff;
}
.dark-theme {
--primary-color: #3eaf7c;
--secondary-color: #273849;
--text-color: #f0f0f0;
--background-color: #121212;
}
.button {
background-color: var(--primary-color);
color: var(--background-color);
}
</style>
总结
Vue 3 带来了巨大的改进,特别是 Composition API,让开发者能够更灵活地组织代码并提高复用性。
本指南介绍了 Vue 3 开发的关键方面:
- 基本概念和新特性
- 组件设计和通信模式
- 状态管理和路由
- 性能优化技巧
- 测试方法
- 最佳实践
通过掌握这些知识和技术,你能够构建出高效、可维护且用户友好的 Vue 应用。随着你的应用变得更加复杂,可以进一步深入探索 Vue 生态系统中的其他工具和库,如 Nuxt.js(用于服务端渲染)、Vueuse(组合式函数集合)以及各种 UI 组件库。