【前端】Vue3详解:现代化的渐进式前端框架
前言
Vue 3是Vue.js框架的第三个主要版本,于2020年9月正式发布。作为一个渐进式JavaScript框架,Vue 3在保持Vue 2核心理念的基础上,引入了许多革命性的改进和新特性。本文将深入探讨Vue 3的核心概念、新特性、最佳实践以及与Vue 2的主要区别,帮助开发者全面掌握这个现代化的前端框架。
一、Vue 3概述
(一)什么是Vue 3
Vue 3是一个用于构建用户界面的渐进式JavaScript框架。它专注于视图层,采用自底向上增量开发的设计理念。Vue 3的目标是通过尽可能简单的API实现响应式数据绑定和组合的视图组件。
(二)Vue 3的核心特性
1. 性能提升
相比Vue 2,Vue 3在性能方面有了显著提升:
- 渲染性能:提升1.3~2倍
- 打包体积:更小的bundle size
- 内存使用:更高效的内存管理
2. Tree-shaking支持
Vue 3支持Tree-shaking,这意味着可以按需引入功能,未使用的代码不会被打包到最终的bundle中:
// 按需引入Vue功能
import { createApp, ref, reactive } from 'vue'
// 只有使用到的功能会被打包
const app = createApp({})
3. 更好的TypeScript支持
Vue 3从底层重新设计,提供了更好的TypeScript支持,包括:
- 更准确的类型推断
- 更好的IDE支持
- 原生TypeScript支持
(三)Vue 3的优势
- 响应式数据绑定:强大的响应式系统确保数据变化自动反映到视图
- 组件化开发:将应用分解为可复用的组件
- 渐进式框架:可以按需引入特性,适应不同规模的项目
- 简洁的模板语法:直观易学的模板语法
- 虚拟DOM:提高渲染性能
- 丰富的生态系统:活跃的社区和完善的工具链
5
二、Composition API详解
(一)什么是Composition API
Composition API是Vue 3引入的一组新的API,用于组织和复用组件逻辑。与传统的Options API不同,Composition API通过函数式的方式将相关的逻辑组合在一起。
(二)Composition API的优势
- 逻辑复用更便捷:通过组合函数可以轻松复用和共享逻辑
- 代码组织更清晰:将相关的状态和行为放在同一个函数中
- TypeScript更友好:提供更好的类型推断
- 解决大型项目维护问题:避免Options API在大型项目中的代码分散问题
1
(三)setup()函数
setup()函数是Vue 3中专门为组件提供的新属性,它为使用Composition API提供了统一的入口:
// 基本的setup函数
export default {
setup(props, context) {
// Attribute (非响应式对象)
console.log(context.attrs)
// 插槽 (非响应式对象)
console.log(context.slots)
// 触发事件 (方法)
console.log(context.emit)
// 返回的数据和方法可以在模板中使用
return {
// 数据和方法
}
}
}
(四)响应式API
1. ref()
ref()
用于创建响应式的基本数据类型:
import { ref } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello Vue 3')
const increment = () => {
count.value++
}
return {
count,
message,
increment
}
}
}
2. reactive()
reactive()
用于创建响应式的对象:
import { reactive } from 'vue'
export default {
setup() {
const state = reactive({
name: 'Vue 3',
version: '3.0',
features: ['Composition API', 'Multiple root nodes']
})
const updateName = (newName) => {
state.name = newName
}
return {
state,
updateName
}
}
}
3. computed()
计算属性在Composition API中的使用:
import { ref, computed } from 'vue'
export default {
setup() {
const firstName = ref('John')
const lastName = ref('Doe')
const fullName = computed(() => {
return `${firstName.value} ${lastName.value}`
})
return {
firstName,
lastName,
fullName
}
}
}
4. watch()和watchEffect()
监听器的使用:
import { ref, watch, watchEffect } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('')
// watch
watch(count, (newValue, oldValue) => {
console.log(`count changed from ${oldValue} to ${newValue}`)
})
// watchEffect
watchEffect(() => {
console.log(`count is ${count.value}`)
})
return {
count,
message
}
}
}
(五)组合函数(Composables)
组合函数是利用Composition API封装和复用有状态逻辑的函数:
// src/composables/useTodos.js
import { ref, computed } from 'vue'
export function useTodos() {
const todos = ref([
{ id: 1, text: '学习 Vue3', completed: false },
{ id: 2, text: '编写 Composition API 教程', completed: false }
])
const newTodo = ref('')
const addTodo = () => {
if (newTodo.value.trim()) {
todos.value.push({
id: Date.now(),
text: newTodo.value,
completed: false
})
newTodo.value = ''
}
}
const removeTodo = (id) => {
todos.value = todos.value.filter(todo => todo.id !== id)
}
const completedCount = computed(() => {
return todos.value.filter(todo => todo.completed).length
})
const totalCount = computed(() => todos.value.length)
return {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
}
}
在组件中使用组合函数:
// TodoApp.vue
import { useTodos } from '@/composables/useTodos'
export default {
setup() {
const {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
} = useTodos()
return {
todos,
newTodo,
addTodo,
removeTodo,
completedCount,
totalCount
}
}
}
三、响应式系统深度解析
(一)Vue 3响应式系统原理
Vue 3使用Proxy API替代了Vue 2中的Object.defineProperty,实现了更强大的响应式系统。
1. Proxy vs Object.defineProperty
Object.defineProperty的局限性:
- 无法检测对象属性的添加和删除
- 数组API方法无法监听
- 需要对每个属性进行遍历监听
- 深层嵌套对象需要深层监听,造成性能问题
Proxy的优势:
- 可以监听整个对象,而不是单个属性
- 支持数组索引和length属性的监听
- 支持Map、Set、WeakMap、WeakSet等数据结构
- 有13种拦截方法,功能更强大
2. 响应式实现原理
// 简化的响应式实现
function reactive(obj) {
if (typeof obj !== 'object' || obj === null) {
return obj
}
// Proxy相当于在对象外层加拦截
const observed = new Proxy(obj, {
get(target, key, receiver) {
const res = Reflect.get(target, key, receiver)
console.log(`获取${key}:${res}`)
// 依赖收集
track(target, key)
// 如果是对象,递归代理
return isObject(res) ? reactive(res) : res
},
set(target, key, value, receiver) {
const oldValue = target[key]
const res = Reflect.set(target, key, value, receiver)
if (oldValue !== value) {
console.log(`设置${key}:${value}`)
// 触发更新
trigger(target, key)
}
return res
},
deleteProperty(target, key) {
const res = Reflect.deleteProperty(target, key)
console.log(`删除${key}:${res}`)
// 触发更新
trigger(target, key)
return res
}
})
return observed
}
3. 依赖收集和触发机制
// 依赖收集和触发的简化实现
const targetMap = new WeakMap()
let activeEffect = null
function track(target, key) {
if (!activeEffect) return
let depsMap = targetMap.get(target)
if (!depsMap) {
targetMap.set(target, (depsMap = new Map()))
}
let dep = depsMap.get(key)
if (!dep) {
depsMap.set(key, (dep = new Set()))
}
dep.add(activeEffect)
}
function trigger(target, key) {
const depsMap = targetMap.get(target)
if (!depsMap) return
const dep = depsMap.get(key)
if (dep) {
dep.forEach(effect => effect())
}
}
function effect(fn) {
activeEffect = fn
fn()
activeEffect = null
}
(二)ref vs reactive
1. ref的实现原理
function ref(value) {
const refObject = {
get value() {
track(refObject, 'value')
return value
},
set value(newValue) {
value = newValue
trigger(refObject, 'value')
}
}
return refObject
}
2. 使用场景对比
// ref适用于基本数据类型
const count = ref(0)
const message = ref('hello')
const isVisible = ref(true)
// reactive适用于对象和数组
const state = reactive({
user: {
name: 'John',
age: 30
},
todos: []
})
// 访问方式不同
console.log(count.value) // ref需要.value
console.log(state.user.name) // reactive直接访问
3. 响应式转换工具
import {
ref,
reactive,
toRef,
toRefs,
unref,
isRef,
isReactive
} from 'vue'
const state = reactive({
name: 'Vue',
version: 3
})
// toRef: 将reactive对象的属性转为ref
const name = toRef(state, 'name')
// toRefs: 将reactive对象的所有属性转为ref
const { name: nameRef, version: versionRef } = toRefs(state)
// unref: 获取ref的值,如果不是ref则直接返回
const value = unref(name) // 等同于 isRef(name) ? name.value : name
// 类型检查
console.log(isRef(name)) // true
console.log(isReactive(state)) // true
(三)Vue 3.2响应式优化
Vue 3.2对响应式系统进行了进一步优化:
- 更高效的ref实现:读取性能提升260%,写入性能提升50%
- 位标记优化:使用位运算优化依赖追踪
- effect嵌套优化:更好地处理effect嵌套场景
四、新增组件和特性
(一)Fragment(片段)
Vue 3支持组件有多个根节点,不再需要单一根元素:
<!-- Vue 2中必须有单一根元素 -->
<template>
<div>
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</div>
</template>
<!-- Vue 3中可以有多个根节点 -->
<template>
<header>Header</header>
<main>Main content</main>
<footer>Footer</footer>
</template>
(二)Teleport(传送门)
Teleport允许我们将组件的一部分模板”传送”到DOM中的其他位置:
<template>
<div class="modal-container">
<button @click="showModal = true">打开模态框</button>
<!-- 将模态框传送到body下 -->
<Teleport to="body">
<div v-if="showModal" class="modal">
<div class="modal-content">
<h3>模态框标题</h3>
<p>模态框内容</p>
<button @click="showModal = false">关闭</button>
</div>
</div>
</Teleport>
</div>
</template>
<script>
import { ref } from 'vue'
export default {
setup() {
const showModal = ref(false)
return {
showModal
}
}
}
</script>
<style>
.modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
}
.modal-content {
background: white;
padding: 20px;
border-radius: 8px;
}
</style>
(三)Suspense(悬念)
Suspense让组件在渲染之前进行”等待”,并在等待时显示fallback内容:
<template>
<Suspense>
<!-- 异步组件 -->
<template #default>
<AsyncComponent />
</template>
<!-- 加载中的fallback内容 -->
<template #fallback>
<div>Loading...</div>
</template>
</Suspense>
</template>
<script>
import { defineAsyncComponent } from 'vue'
// 定义异步组件
const AsyncComponent = defineAsyncComponent(() => {
return new Promise((resolve) => {
setTimeout(() => {
resolve({
template: '<div>异步组件加载完成!</div>'
})
}, 2000)
})
})
export default {
components: {
AsyncComponent
}
}
</script>
异步组件的高级用法
import { defineAsyncComponent } from 'vue'
// 带选项的异步组件
const AsyncComponent = defineAsyncComponent({
// 工厂函数
loader: () => import('./AsyncComponent.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
五、Vue 3与Vue 2的主要区别
(一)API设计变化
1. 全局API变化
// Vue 2
import Vue from 'vue'
import App from './App.vue'
Vue.config.productionTip = false
Vue.use(SomePlugin)
Vue.mixin(SomeMixin)
Vue.component('GlobalComponent', SomeComponent)
new Vue({
render: h => h(App)
}).$mount('#app')
// Vue 3
import { createApp } from 'vue'
import App from './App.vue'
const app = createApp(App)
app.config.globalProperties.customProperty = 'custom value'
app.use(SomePlugin)
app.mixin(SomeMixin)
app.component('GlobalComponent', SomeComponent)
app.mount('#app')
2. 组件定义变化
// Vue 2 Options API
export default {
data() {
return {
count: 0,
message: 'Hello'
}
},
computed: {
doubleCount() {
return this.count * 2
}
},
methods: {
increment() {
this.count++
}
},
mounted() {
console.log('Component mounted')
}
}
// Vue 3 Composition API
import { ref, computed, onMounted } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('Hello')
const doubleCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
onMounted(() => {
console.log('Component mounted')
})
return {
count,
message,
doubleCount,
increment
}
}
}
(二)生命周期变化
// Vue 2生命周期
export default {
beforeCreate() {},
created() {},
beforeMount() {},
mounted() {},
beforeUpdate() {},
updated() {},
beforeDestroy() {},
destroyed() {}
}
// Vue 3 Composition API生命周期
import {
onBeforeMount,
onMounted,
onBeforeUpdate,
onUpdated,
onBeforeUnmount,
onUnmounted
} from 'vue'
export default {
setup() {
// 注意:没有beforeCreate和created对应的组合式API
// 因为setup()就相当于这两个钩子
onBeforeMount(() => {
console.log('Before mount')
})
onMounted(() => {
console.log('Mounted')
})
onBeforeUpdate(() => {
console.log('Before update')
})
onUpdated(() => {
console.log('Updated')
})
onBeforeUnmount(() => {
console.log('Before unmount')
})
onUnmounted(() => {
console.log('Unmounted')
})
}
}
(三)v-model变化
Vue 3中的v-model有了重大改变:
// Vue 2中的v-model
// 父组件
<CustomInput v-model="searchText" />
// 子组件
export default {
props: ['value'],
methods: {
updateValue(value) {
this.$emit('input', value)
}
}
}
// Vue 3中的v-model
// 父组件
<CustomInput v-model="searchText" />
// 子组件
export default {
props: ['modelValue'],
emits: ['update:modelValue'],
methods: {
updateValue(value) {
this.$emit('update:modelValue', value)
}
}
}
// Vue 3支持多个v-model
<UserName
v-model:first-name="firstName"
v-model:last-name="lastName"
/>
(四)移除的特性
- 过滤器(Filters):Vue 3中移除了过滤器,建议使用计算属性或方法替代
- $children:移除了$children属性
- 事件API:移除了$on、$off、$once等事件API
- 内联模板:移除了inline-template特性
六、Vue 3生态系统
(一)官方库
1. Vue Router 4
// 安装
npm install vue-router@4
// 使用
import { createRouter, createWebHistory } from 'vue-router'
import Home from './components/Home.vue'
import About from './components/About.vue'
const routes = [
{ path: '/', component: Home },
{ path: '/about', component: About }
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
2. Pinia(状态管理)
Pinia是Vue 3推荐的状态管理库,替代Vuex:
// 安装
npm install pinia
// 定义store
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
state: () => ({
count: 0
}),
getters: {
doubleCount: (state) => state.count * 2
},
actions: {
increment() {
this.count++
}
}
})
// 在组件中使用
import { useCounterStore } from '@/stores/counter'
export default {
setup() {
const counter = useCounterStore()
return {
counter
}
}
}
3. Vue DevTools
Vue 3专用的开发者工具,支持:
- Composition API调试
- 时间旅行调试
- 组件检查器
- 性能分析
(二)构建工具
1. Vite
Vite是Vue 3推荐的构建工具:
# 创建Vue 3项目
npm create vite@latest my-vue-app -- --template vue
# 或者使用TypeScript模板
npm create vite@latest my-vue-app -- --template vue-ts
Vite的特点:
- 极快的冷启动
- 即时的模块热更新
- 真正的按需编译
- 丰富的插件生态
2. Vite配置示例
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src')
}
},
server: {
port: 3000,
open: true
},
build: {
outDir: 'dist',
sourcemap: true,
rollupOptions: {
output: {
manualChunks: {
vendor: ['vue', 'vue-router'],
utils: ['lodash', 'axios']
}
}
}
}
})
七、Vue 3最佳实践
(一)组件设计原则
1. 单一职责原则
<!-- 好的做法:职责单一的组件 -->
<template>
<div class="user-card">
<img :src="user.avatar" :alt="user.name" />
<h3>{{ user.name }}</h3>
<p>{{ user.email }}</p>
</div>
</template>
<script>
export default {
props: {
user: {
type: Object,
required: true
}
}
}
</script>
2. Props验证
export default {
props: {
// 基础类型检查
title: String,
// 多种可能的类型
count: [Number, String],
// 必填的字符串
message: {
type: String,
required: true
},
// 带有默认值的数字
size: {
type: Number,
default: 100
},
// 带有默认值的对象
user: {
type: Object,
default: () => ({
name: 'Guest',
email: ''
})
},
// 自定义验证函数
status: {
type: String,
validator: (value) => {
return ['pending', 'success', 'error'].includes(value)
}
}
}
}
3. 事件命名规范
<template>
<button @click="handleClick">点击我</button>
</template>
<script>
export default {
emits: {
// 验证事件
'user-updated': (payload) => {
return payload && typeof payload.id === 'number'
},
// 简单声明
'item-selected': null
},
methods: {
handleClick() {
// 使用kebab-case命名事件
this.$emit('user-updated', { id: 1, name: 'John' })
}
}
}
</script>
(二)Composition API最佳实践
1. 逻辑分组
// 好的做法:按功能分组
export default {
setup() {
// 用户相关逻辑
const { user, updateUser, fetchUser } = useUser()
// 搜索相关逻辑
const { searchQuery, searchResults, performSearch } = useSearch()
// 分页相关逻辑
const { currentPage, totalPages, changePage } = usePagination()
return {
// 用户
user,
updateUser,
fetchUser,
// 搜索
searchQuery,
searchResults,
performSearch,
// 分页
currentPage,
totalPages,
changePage
}
}
}
2. 组合函数设计
// composables/useApi.js
import { ref, reactive } from 'vue'
import axios from 'axios'
export function useApi(url) {
const data = ref(null)
const error = ref(null)
const loading = ref(false)
const execute = async (config = {}) => {
try {
loading.value = true
error.value = null
const response = await axios({
url,
...config
})
data.value = response.data
return response.data
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
return {
data: readonly(data),
error: readonly(error),
loading: readonly(loading),
execute
}
}
// 使用
export default {
setup() {
const { data: users, loading, error, execute } = useApi('/api/users')
const fetchUsers = () => execute({ method: 'GET' })
onMounted(fetchUsers)
return {
users,
loading,
error,
fetchUsers
}
}
}
3. 响应式数据管理
// 好的做法:合理使用ref和reactive
export default {
setup() {
// 基本类型使用ref
const count = ref(0)
const message = ref('')
const isVisible = ref(true)
// 对象使用reactive
const form = reactive({
name: '',
email: '',
age: 0
})
// 数组使用reactive
const items = reactive([])
// 避免解构reactive对象
// 错误做法
// const { name, email } = form // 失去响应性
// 正确做法
const { name, email } = toRefs(form)
return {
count,
message,
isVisible,
form,
items,
name,
email
}
}
}
(三)性能优化
1. 组件懒加载
// 路由懒加载
const routes = [
{
path: '/home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
component: () => import('@/views/About.vue')
}
]
// 组件懒加载
import { defineAsyncComponent } from 'vue'
export default {
components: {
HeavyComponent: defineAsyncComponent(() =>
import('@/components/HeavyComponent.vue')
)
}
}
2. 使用v-memo优化列表渲染
<template>
<div>
<!-- 使用v-memo缓存列表项 -->
<div
v-for="item in list"
:key="item.id"
v-memo="[item.id, item.name, item.status]"
>
<span>{{ item.name }}</span>
<span>{{ item.status }}</span>
</div>
</div>
</template>
3. 合理使用计算属性
export default {
setup() {
const items = ref([])
const filter = ref('')
// 使用计算属性缓存过滤结果
const filteredItems = computed(() => {
if (!filter.value) return items.value
return items.value.filter(item =>
item.name.toLowerCase().includes(filter.value.toLowerCase())
)
})
return {
items,
filter,
filteredItems
}
}
}
八、Vue 3项目实战
(一)项目结构
src/
├── assets/ # 静态资源
├── components/ # 公共组件
│ ├── common/ # 通用组件
│ └── ui/ # UI组件
├── composables/ # 组合函数
├── directives/ # 自定义指令
├── plugins/ # 插件
├── router/ # 路由配置
├── stores/ # 状态管理
├── utils/ # 工具函数
├── views/ # 页面组件
├── App.vue # 根组件
└── main.js # 入口文件
(二)实战示例:Todo应用
1. 创建Todo Store
// stores/todos.js
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useTodosStore = defineStore('todos', () => {
const todos = ref([])
const filter = ref('all') // all, active, completed
const filteredTodos = computed(() => {
switch (filter.value) {
case 'active':
return todos.value.filter(todo => !todo.completed)
case 'completed':
return todos.value.filter(todo => todo.completed)
default:
return todos.value
}
})
const activeTodosCount = computed(() => {
return todos.value.filter(todo => !todo.completed).length
})
const addTodo = (text) => {
todos.value.push({
id: Date.now(),
text,
completed: false,
createdAt: new Date()
})
}
const removeTodo = (id) => {
const index = todos.value.findIndex(todo => todo.id === id)
if (index > -1) {
todos.value.splice(index, 1)
}
}
const toggleTodo = (id) => {
const todo = todos.value.find(todo => todo.id === id)
if (todo) {
todo.completed = !todo.completed
}
}
const clearCompleted = () => {
todos.value = todos.value.filter(todo => !todo.completed)
}
const setFilter = (newFilter) => {
filter.value = newFilter
}
return {
todos,
filter,
filteredTodos,
activeTodosCount,
addTodo,
removeTodo,
toggleTodo,
clearCompleted,
setFilter
}
})
2. Todo组件
<!-- components/TodoItem.vue -->
<template>
<li class="todo-item" :class="{ completed: todo.completed }">
<input
type="checkbox"
:checked="todo.completed"
@change="$emit('toggle', todo.id)"
/>
<span
v-if="!editing"
class="todo-text"
@dblclick="startEdit"
>
{{ todo.text }}
</span>
<input
v-else
ref="editInput"
v-model="editText"
class="edit-input"
@blur="finishEdit"
@keyup.enter="finishEdit"
@keyup.esc="cancelEdit"
/>
<button
class="delete-btn"
@click="$emit('remove', todo.id)"
>
×
</button>
</li>
</template>
<script>
import { ref, nextTick } from 'vue'
export default {
props: {
todo: {
type: Object,
required: true
}
},
emits: ['toggle', 'remove', 'update'],
setup(props, { emit }) {
const editing = ref(false)
const editText = ref('')
const editInput = ref(null)
const startEdit = () => {
editing.value = true
editText.value = props.todo.text
nextTick(() => {
editInput.value?.focus()
})
}
const finishEdit = () => {
if (editing.value) {
const text = editText.value.trim()
if (text) {
emit('update', props.todo.id, text)
} else {
emit('remove', props.todo.id)
}
editing.value = false
}
}
const cancelEdit = () => {
editing.value = false
editText.value = props.todo.text
}
return {
editing,
editText,
editInput,
startEdit,
finishEdit,
cancelEdit
}
}
}
</script>
<style scoped>
.todo-item {
display: flex;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.todo-item.completed .todo-text {
text-decoration: line-through;
color: #999;
}
.todo-text {
flex: 1;
margin: 0 10px;
cursor: pointer;
}
.edit-input {
flex: 1;
margin: 0 10px;
padding: 5px;
border: 1px solid #ddd;
border-radius: 3px;
}
.delete-btn {
background: #ff4757;
color: white;
border: none;
border-radius: 3px;
padding: 5px 10px;
cursor: pointer;
}
.delete-btn:hover {
background: #ff3838;
}
</style>
3. 主应用组件
<!-- App.vue -->
<template>
<div class="todo-app">
<header class="header">
<h1>Todo App</h1>
<input
v-model="newTodo"
class="new-todo"
placeholder="What needs to be done?"
@keyup.enter="addTodo"
/>
</header>
<main class="main" v-if="todos.length">
<ul class="todo-list">
<TodoItem
v-for="todo in filteredTodos"
:key="todo.id"
:todo="todo"
@toggle="toggleTodo"
@remove="removeTodo"
@update="updateTodo"
/>
</ul>
</main>
<footer class="footer" v-if="todos.length">
<span class="todo-count">
{{ activeTodosCount }} item{{ activeTodosCount !== 1 ? 's' : '' }} left
</span>
<div class="filters">
<button
v-for="filterName in ['all', 'active', 'completed']"
:key="filterName"
:class="{ active: filter === filterName }"
@click="setFilter(filterName)"
>
{{ filterName }}
</button>
</div>
<button
v-if="todos.length > activeTodosCount"
class="clear-completed"
@click="clearCompleted"
>
Clear completed
</button>
</footer>
</div>
</template>
<script>
import { ref } from 'vue'
import { storeToRefs } from 'pinia'
import { useTodosStore } from '@/stores/todos'
import TodoItem from '@/components/TodoItem.vue'
export default {
components: {
TodoItem
},
setup() {
const todosStore = useTodosStore()
const newTodo = ref('')
const {
todos,
filter,
filteredTodos,
activeTodosCount
} = storeToRefs(todosStore)
const {
addTodo: addTodoToStore,
removeTodo,
toggleTodo,
clearCompleted,
setFilter
} = todosStore
const addTodo = () => {
const text = newTodo.value.trim()
if (text) {
addTodoToStore(text)
newTodo.value = ''
}
}
const updateTodo = (id, text) => {
const todo = todos.value.find(t => t.id === id)
if (todo) {
todo.text = text
}
}
return {
newTodo,
todos,
filter,
filteredTodos,
activeTodosCount,
addTodo,
removeTodo,
toggleTodo,
updateTodo,
clearCompleted,
setFilter
}
}
}
</script>
<style>
.todo-app {
max-width: 600px;
margin: 0 auto;
padding: 20px;
font-family: Arial, sans-serif;
}
.header h1 {
text-align: center;
color: #333;
margin-bottom: 20px;
}
.new-todo {
width: 100%;
padding: 15px;
font-size: 16px;
border: 1px solid #ddd;
border-radius: 5px;
margin-bottom: 20px;
}
.todo-list {
list-style: none;
padding: 0;
margin: 0;
border: 1px solid #ddd;
border-radius: 5px;
}
.footer {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px 0;
margin-top: 20px;
}
.filters button {
margin: 0 5px;
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
border-radius: 3px;
cursor: pointer;
}
.filters button.active {
background: #007bff;
color: white;
}
.clear-completed {
padding: 5px 10px;
border: 1px solid #ddd;
background: white;
border-radius: 3px;
cursor: pointer;
}
.clear-completed:hover {
background: #f8f9fa;
}
</style>
九、构建与部署
(一)Vite构建配置
// vite.config.js
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: {
outDir: 'dist',
assetsDir: 'assets',
sourcemap: false,
// 代码分割
rollupOptions: {
output: {
manualChunks: {
// 将Vue相关库打包到vendor chunk
vendor: ['vue', 'vue-router', 'pinia'],
// 将UI库单独打包
ui: ['element-plus'],
// 将工具库单独打包
utils: ['lodash', 'axios', 'dayjs']
}
}
},
// 压缩配置
minify: 'terser',
terserOptions: {
compress: {
drop_console: true,
drop_debugger: true
}
}
},
// 开发服务器配置
server: {
port: 3000,
open: true,
cors: true,
// 代理配置
proxy: {
'/api': {
target: 'http://localhost:8080',
changeOrigin: true,
rewrite: (path) => path.replace(/^\/api/, '')
}
}
}
})
(二)性能优化配置
1. 静态资源优化
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { visualizer } from 'rollup-plugin-visualizer'
import viteCompression from 'vite-plugin-compression'
export default defineConfig({
plugins: [
vue(),
// 打包分析
visualizer({
filename: 'dist/stats.html',
open: true
}),
// Gzip压缩
viteCompression({
verbose: true,
disable: false,
threshold: 10240,
algorithm: 'gzip',
ext: '.gz'
})
],
build: {
rollupOptions: {
output: {
// 静态资源分类
assetFileNames: (assetInfo) => {
const info = assetInfo.name.split('.')
let extType = info[info.length - 1]
if (/\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'media'
} else if (/\.(png|jpe?g|gif|svg)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'img'
} else if (/\.(woff2?|eot|ttf|otf)(\?.*)?$/i.test(assetInfo.name)) {
extType = 'fonts'
}
return `assets/${extType}/[name]-[hash][extname]`
},
// JS文件分类
chunkFileNames: 'assets/js/[name]-[hash].js',
entryFileNames: 'assets/js/[name]-[hash].js'
}
}
}
})
2. CDN配置
// vite.config.js
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
build: {
rollupOptions: {
external: ['vue', 'vue-router', 'pinia'],
output: {
globals: {
vue: 'Vue',
'vue-router': 'VueRouter',
pinia: 'Pinia'
}
}
}
}
})
<!-- index.html -->
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Vue 3 App</title>
<!-- CDN引入 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script src="https://unpkg.com/vue-router@4/dist/vue-router.global.js"></script>
<script src="https://unpkg.com/pinia@2/dist/pinia.iife.js"></script>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.js"></script>
</body>
</html>
(三)部署配置
1. Nginx配置
server {
listen 80;
server_name your-domain.com;
root /var/www/vue-app/dist;
index index.html;
# Gzip压缩
gzip on;
gzip_vary on;
gzip_min_length 1024;
gzip_types text/plain text/css text/xml text/javascript application/javascript application/xml+rss application/json;
# 静态资源缓存
location ~* \.(js|css|png|jpg|jpeg|gif|ico|svg)$ {
expires 1y;
add_header Cache-Control "public, immutable";
}
# HTML文件不缓存
location ~* \.html$ {
expires -1;
add_header Cache-Control "no-cache, no-store, must-revalidate";
}
# SPA路由支持
location / {
try_files $uri $uri/ /index.html;
}
# API代理
location /api/ {
proxy_pass http://backend-server:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
}
}
2. Docker部署
# Dockerfile
# 构建阶段
FROM node:18-alpine as build-stage
WORKDIR /app
# 复制package文件
COPY package*.json ./
# 安装依赖
RUN npm ci --only=production
# 复制源代码
COPY . .
# 构建应用
RUN npm run build
# 生产阶段
FROM nginx:alpine as production-stage
# 复制构建结果
COPY /app/dist /usr/share/nginx/html
# 复制nginx配置
COPY nginx.conf /etc/nginx/conf.d/default.conf
EXPOSE 80
CMD ["nginx", "-g", "daemon off;"]
# docker-compose.yml
version: '3.8'
services:
vue-app:
build: .
ports:
- "80:80"
environment:
- NODE_ENV=production
restart: unless-stopped
十、性能优化实践
(一)代码层面优化
1. 使用shallowRef和shallowReactive
对于大型数据结构,使用浅层响应式可以提高性能:
import { ref, shallowRef, reactive, shallowReactive } from 'vue'
export default {
setup() {
// 对于大型数据结构,使用shallowRef
const largeData = shallowRef({
items: new Array(10000).fill(0).map((_, i) => ({ id: i, name: `Item ${i}` }))
})
// 普通ref会深度监听,性能较差
// const largeData = ref({ items: [...] }) // 避免这样做
// 更新数据时,需要替换整个对象
const updateData = () => {
largeData.value = {
items: largeData.value.items.concat({ id: Date.now(), name: 'New Item' })
}
}
return {
largeData,
updateData
}
}
}
2. 使用v-once和v-memo优化静态内容
<template>
<div>
<!-- 静态内容使用v-once -->
<h1 v-once>{{ title }}</h1>
<!-- 使用v-memo缓存复杂计算 -->
<div v-memo="[user.id, user.name]">
<UserProfile :user="user" />
</div>
<!-- 列表优化 -->
<div
v-for="item in items"
:key="item.id"
v-memo="[item.id, item.status, item.priority]"
>
<ExpensiveComponent :item="item" />
</div>
</div>
</template>
3. 异步组件和代码分割
// 路由级别的代码分割
const routes = [
{
path: '/dashboard',
component: () => import('@/views/Dashboard.vue')
},
{
path: '/profile',
component: () => import('@/views/Profile.vue')
}
]
// 组件级别的懒加载
import { defineAsyncComponent } from 'vue'
const HeavyChart = defineAsyncComponent({
loader: () => import('@/components/HeavyChart.vue'),
loadingComponent: () => '<div>Loading chart...</div>',
errorComponent: () => '<div>Failed to load chart</div>',
delay: 200,
timeout: 3000
})
(二)构建优化
1. Tree Shaking优化
// 按需引入工具库
import { debounce, throttle } from 'lodash-es'
// 而不是
// import _ from 'lodash'
// 按需引入UI库
import { ElButton, ElInput } from 'element-plus'
// 而不是
// import ElementPlus from 'element-plus'
2. 预加载和预获取
// vite.config.js
export default defineConfig({
build: {
rollupOptions: {
output: {
manualChunks: {
// 关键路径组件
critical: ['@/views/Home.vue', '@/views/Login.vue'],
// 次要组件
secondary: ['@/views/Profile.vue', '@/views/Settings.vue']
}
}
}
}
})
<!-- 在路由组件中预加载 -->
<template>
<div>
<router-link
to="/profile"
@mouseenter="preloadProfile"
>
Profile
</router-link>
</div>
</template>
<script>
export default {
methods: {
preloadProfile() {
// 预加载Profile组件
import('@/views/Profile.vue')
}
}
}
</script>
(三)运行时优化
1. 虚拟滚动
<!-- VirtualList.vue -->
<template>
<div
ref="container"
class="virtual-list"
@scroll="handleScroll"
>
<div
class="virtual-list-phantom"
:style="{ height: totalHeight + 'px' }"
></div>
<div
class="virtual-list-content"
:style="{ transform: `translateY(${offset}px)` }"
>
<div
v-for="item in visibleItems"
:key="item.id"
class="virtual-list-item"
:style="{ height: itemHeight + 'px' }"
>
<slot :item="item"></slot>
</div>
</div>
</div>
</template>
<script>
import { ref, computed, onMounted } from 'vue'
export default {
props: {
items: Array,
itemHeight: {
type: Number,
default: 50
},
visibleCount: {
type: Number,
default: 10
}
},
setup(props) {
const container = ref(null)
const scrollTop = ref(0)
const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
const startIndex = computed(() => {
return Math.floor(scrollTop.value / props.itemHeight)
})
const endIndex = computed(() => {
return Math.min(
startIndex.value + props.visibleCount,
props.items.length
)
})
const visibleItems = computed(() => {
return props.items.slice(startIndex.value, endIndex.value)
})
const offset = computed(() => {
return startIndex.value * props.itemHeight
})
const handleScroll = (e) => {
scrollTop.value = e.target.scrollTop
}
return {
container,
totalHeight,
visibleItems,
offset,
handleScroll
}
}
}
</script>
2. 防抖和节流
// composables/useDebounce.js
import { ref, watch } from 'vue'
export function useDebounce(value, delay = 300) {
const debouncedValue = ref(value.value)
watch(value, (newValue) => {
const timer = setTimeout(() => {
debouncedValue.value = newValue
}, delay)
return () => clearTimeout(timer)
})
return debouncedValue
}
// 使用
export default {
setup() {
const searchQuery = ref('')
const debouncedQuery = useDebounce(searchQuery, 500)
watch(debouncedQuery, (newQuery) => {
// 执行搜索
performSearch(newQuery)
})
return {
searchQuery
}
}
}
十一、总结
Vue 3作为Vue.js框架的重大升级版本,带来了许多革命性的改进和新特性。通过本文的深入探讨,我们可以看到Vue 3在以下几个方面的显著优势:
(一)核心特性总结
- Composition API:提供了更灵活的逻辑组织方式,解决了大型项目中代码复用和维护的问题
- 响应式系统优化:基于Proxy的实现提供了更强大的响应式能力和更好的性能
- 更好的TypeScript支持:从底层设计就考虑了TypeScript集成,提供更准确的类型推断
- Tree-shaking友好:支持按需引入,显著减小打包体积
- Fragment支持:允许组件有多个根节点,提供更灵活的模板结构
- 新增组件:Teleport、Suspense等新组件解决了特定场景下的开发需求
(二)开发建议
- 渐进式迁移:对于现有Vue 2项目,可以逐步引入Vue 3特性,不必一次性重写
- 合理使用Composition API:在复杂组件和需要逻辑复用的场景下使用,简单组件仍可使用Options API
- 性能优化:充分利用Vue 3的性能优化特性,如shallowRef、v-memo等
- 状态管理:推荐使用Pinia替代Vuex,享受更好的TypeScript支持和开发体验
- 构建工具:使用Vite作为构建工具,获得更快的开发体验
(三)学习路径建议
- 基础阶段:掌握Vue 3基本概念、响应式系统、组件开发
- 进阶阶段:深入学习Composition API、自定义组合函数、高级组件模式
- 实战阶段:结合Vue Router 4、Pinia等生态库开发完整项目
- 优化阶段:学习性能优化技巧、构建优化、部署最佳实践
Vue 3不仅保持了Vue.js一贯的简洁易学特点,还在性能、开发体验和生态系统方面都有了显著提升。随着生态系统的不断完善,Vue 3必将成为现代前端开发的重要选择。