Vue核心特性与进阶:组件的高级玩法
在掌握了Vue基础之后,我们现在来探索一些更高级的特性和功能。这些特性就像给你的Vue应用装上"涡轮增压器",让开发效率和应用性能都得到显著提升。
1. 组件进阶:生命周期与插槽魔法
组件生命周期:组件的"人生轨迹"
Vue组件就像人一样,有出生、成长、工作、退休等不同阶段:
vue
<script>
export default {
name: 'LifecycleDemo',
// 1. 创建阶段
beforeCreate() {
console.log('1. beforeCreate - 实例创建前')
// 此时data、methods等还无法访问
},
created() {
console.log('2. created - 实例创建后')
// data、methods已初始化,但DOM还未挂载
// 适合进行数据初始化、API调用
},
beforeMount() {
console.log('3. beforeMount - 挂载前')
// 模板编译完成,但还未挂载到DOM
},
mounted() {
console.log('4. mounted - 挂载后')
// DOM已挂载,可以访问DOM元素
// 适合进行DOM操作、启动定时器
},
// 2. 更新阶段
beforeUpdate() {
console.log('5. beforeUpdate - 更新前')
// 数据更新,但DOM还未重新渲染
},
updated() {
console.log('6. updated - 更新后')
// DOM已更新完成
},
// 3. 销毁阶段
beforeDestroy() {
console.log('7. beforeDestroy - 销毁前')
// 实例销毁前,仍可访问实例
// 适合清理工作(如清除定时器、取消订阅)
},
destroyed() {
console.log('8. destroyed - 销毁后')
// 实例已销毁,所有事件监听器被移除
}
}
</script>Vue 3 Composition API中的生命周期:
vue
<script setup>
import { onMounted, onUpdated, onUnmounted } from 'vue'
// 创建阶段
console.log('setup - 组件创建')
onMounted(() => {
console.log('mounted - 组件挂载')
// 相当于选项式API的mounted
})
onUpdated(() => {
console.log('updated - 组件更新')
// 相当于选项式API的updated
})
onUnmounted(() => {
console.log('unmounted - 组件卸载')
// 相当于选项式API的destroyed
})
// 其他生命周期钩子:
// onBeforeMount, onBeforeUpdate, onBeforeUnmount
</script>组件插槽:内容分发的艺术
插槽就像组件的"预留空间",让父组件可以向子组件插入内容:
匿名插槽
vue
<!-- Card.vue 子组件 -->
<template>
<div class="card">
<div class="card-header">
<h3>{{ title }}</h3>
</div>
<div class="card-body">
<!-- 匿名插槽 -->
<slot></slot>
</div>
<div class="card-footer">
<!-- 默认内容 -->
<slot name="footer">
<p>默认底部内容</p>
</slot>
</div>
</div>
</template>
<script>
export default {
props: ['title']
}
</script>vue
<!-- 父组件使用 -->
<template>
<card title="我的卡片">
<!-- 插入到匿名插槽 -->
<p>这是卡片的主要内容</p>
<button>点击我</button>
<!-- 具名插槽 -->
<template #footer>
<button @click="handleAction">底部操作</button>
</template>
</card>
</template>具名插槽
vue
<!-- Layout.vue 布局组件 -->
<template>
<div class="layout">
<header class="header">
<slot name="header">
<h1>默认头部</h1>
</slot>
</header>
<main class="main">
<slot></slot> <!-- 默认插槽 -->
</main>
<aside class="sidebar">
<slot name="sidebar"></slot>
</aside>
<footer class="footer">
<slot name="footer"></slot>
</footer>
</div>
</template>vue
<!-- 使用具名插槽 -->
<template>
<layout>
<template #header>
<nav>导航菜单</nav>
</template>
<template #default>
<article>主要内容</article>
</template>
<template #sidebar>
<div>侧边栏内容</div>
</template>
<template #footer>
<p>© 2024 我的网站</p>
</template>
</layout>
</template>作用域插槽:传递数据给插槽
vue
<!-- UserList.vue 组件 -->
<template>
<div class="user-list">
<div v-for="user in users" :key="user.id" class="user-item">
<!-- 作用域插槽,传递数据 -->
<slot :user="user" :index="index" :isVip="user.vip">
<!-- 默认内容 -->
<p>{{ user.name }}</p>
</slot>
</div>
</div>
</template>
<script>
export default {
data() {
return {
users: [
{ id: 1, name: '张三', vip: true },
{ id: 2, name: '李四', vip: false },
{ id: 3, name: '王五', vip: true }
]
}
}
}
</script>vue
<!-- 父组件使用作用域插槽 -->
<template>
<user-list>
<template #default="{ user, index, isVip }">
<div class="custom-user-item">
<span class="index">{{ index + 1 }}.</span>
<span :class="{ 'vip': isVip }">{{ user.name }}</span>
<span v-if="isVip" class="badge">VIP</span>
</div>
</template>
</user-list>
</template>动态组件:组件的"变形术"
vue
<template>
<div>
<!-- 动态组件 -->
<component :is="currentComponent" :message="message"></component>
<!-- 切换组件 -->
<button @click="switchComponent('ComponentA')">组件A</button>
<button @click="switchComponent('ComponentB')">组件B</button>
<button @click="switchComponent('ComponentC')">组件C</button>
</div>
</template>
<script>
import ComponentA from './ComponentA.vue'
import ComponentB from './ComponentB.vue'
import ComponentC from './ComponentC.vue'
export default {
components: {
ComponentA,
ComponentB,
ComponentC
},
data() {
return {
currentComponent: 'ComponentA',
message: 'Hello from parent'
}
},
methods: {
switchComponent(componentName) {
this.currentComponent = componentName
}
}
}
</script>异步组件:按需加载
vue
<script>
// 1. 基本异步组件
const AsyncComponent = () => import('./AsyncComponent.vue')
// 2. 带配置的异步组件
const AsyncComponent = () => ({
// 需要加载的组件
component: import('./AsyncComponent.vue'),
// 加载中显示的组件
loading: LoadingComponent,
// 加载失败显示的组件
error: ErrorComponent,
// 延迟显示加载组件的时间
delay: 200,
// 超时时间
timeout: 3000
})
// 3. 在路由中使用异步组件
const routes = [
{
path: '/about',
component: () => import('./views/About.vue')
}
]
</script>2. 状态管理基础:数据的中央调度室
Vuex(Vue 2)核心概念
Vuex就像应用的"中央数据库",统一管理所有组件的状态:
javascript
// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)
export default new Vuex.Store({
// 1. State - 存储应用状态
state: {
count: 0,
user: null,
products: []
},
// 2. Getters - 计算属性
getters: {
doubleCount: state => state.count * 2,
isLoggedIn: state => !!state.user,
vipProducts: state => state.products.filter(p => p.isVip)
},
// 3. Mutations - 同步修改状态(唯一能修改state的方式)
mutations: {
INCREMENT(state) {
state.count++
},
SET_USER(state, user) {
state.user = user
},
ADD_PRODUCT(state, product) {
state.products.push(product)
}
},
// 4. Actions - 异步操作
actions: {
async login({ commit }, credentials) {
try {
const user = await api.login(credentials)
commit('SET_USER', user)
return user
} catch (error) {
throw error
}
},
async fetchProducts({ commit }) {
const products = await api.getProducts()
commit('SET_PRODUCTS', products)
}
},
// 5. Modules - 模块化
modules: {
cart: cartModule,
user: userModule
}
})vue
<!-- 在组件中使用Vuex -->
<template>
<div>
<p>计数: {{ count }}</p>
<p>双倍计数: {{ doubleCount }}</p>
<button @click="increment">增加</button>
<button @click="login">登录</button>
</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
computed: {
// 1. 直接访问state
count() {
return this.$store.state.count
},
// 2. 使用mapState辅助函数
...mapState(['count']),
// 3. 直接访问getters
doubleCount() {
return this.$store.getters.doubleCount
},
// 4. 使用mapGetters辅助函数
...mapGetters(['doubleCount', 'isLoggedIn'])
},
methods: {
// 1. 直接提交mutation
increment() {
this.$store.commit('INCREMENT')
},
// 2. 使用mapMutations辅助函数
...mapMutations(['INCREMENT']),
// 3. 直接分发action
async login() {
try {
await this.$store.dispatch('login', { username: 'user', password: 'pass' })
} catch (error) {
console.error('登录失败:', error)
}
},
// 4. 使用mapActions辅助函数
...mapActions(['login', 'fetchProducts'])
}
}
</script>Pinia(Vue 3推荐):现代化的状态管理
Pinia是Vue 3的官方状态管理库,API更简洁:
javascript
// store/useCounter.js
import { defineStore } from 'pinia'
export const useCounterStore = defineStore('counter', {
// state
state: () => ({
count: 0,
name: 'Eduardo'
}),
// getters
getters: {
doubleCount: (state) => state.count * 2,
doubleCountPlusOne(): number {
return this.doubleCount + 1
}
},
// actions
actions: {
increment() {
this.count++
},
async fetchUser(userId) {
try {
const userData = await api.getUser(userId)
this.name = userData.name
} catch (error) {
console.error('获取用户失败:', error)
}
}
}
})vue
<!-- 在组件中使用Pinia -->
<template>
<div>
<p>计数: {{ counter.count }}</p>
<p>双倍计数: {{ counter.doubleCount }}</p>
<button @click="counter.increment">增加</button>
<button @click="handleFetchUser">获取用户</button>
</div>
</template>
<script setup>
import { useCounterStore } from '@/store/useCounter'
// 使用store
const counter = useCounterStore()
// 解构store(保持响应性)
const { count, doubleCount } = storeToRefs(counter)
// 解构actions
const { increment, fetchUser } = counter
const handleFetchUser = async () => {
await fetchUser(123)
}
</script>3. 路由管理(Vue Router):SPA的导航系统
基础路由配置
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'
const routes = [
{
path: '/',
name: 'Home',
component: Home
},
{
path: '/about',
name: 'About',
component: About
},
{
path: '/user/:id',
name: 'User',
component: () => import('@/views/User.vue'),
props: true // 将路由参数作为props传递
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/NotFound.vue')
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default routervue
<!-- App.vue -->
<template>
<div id="app">
<nav>
<router-link to="/">首页</router-link>
<router-link to="/about">关于</router-link>
<router-link :to="{ name: 'User', params: { id: 123 } }">
用户123
</router-link>
</nav>
<!-- 路由出口 -->
<router-view></router-view>
</div>
</template>动态路由与嵌套路由
javascript
// 嵌套路由配置
const routes = [
{
path: '/user/:id',
component: User,
children: [
{
// /user/:id/profile
path: 'profile',
component: UserProfile
},
{
// /user/:id/posts
path: 'posts',
component: UserPosts
}
]
}
]vue
<!-- User.vue 父组件 -->
<template>
<div class="user">
<h2>用户 {{ $route.params.id }}</h2>
<router-link :to="`/user/${$route.params.id}/profile`">
个人资料
</router-link>
<router-link :to="`/user/${$route.params.id}/posts`">
文章列表
</router-link>
<!-- 子路由出口 -->
<router-view></router-view>
</div>
</template>路由守卫:访问控制
javascript
// 全局前置守卫
router.beforeEach((to, from, next) => {
// 检查是否需要登录
if (to.meta.requiresAuth && !isAuthenticated()) {
next('/login')
} else {
next()
}
})
// 路由独享守卫
const routes = [
{
path: '/admin',
component: Admin,
beforeEnter: (to, from, next) => {
// 只有管理员才能访问
if (isAdmin()) {
next()
} else {
next('/403')
}
}
}
]
// 组件内守卫
export default {
// 进入路由前
async beforeRouteEnter(to, from, next) {
try {
const data = await fetchData()
next(vm => {
vm.data = data
})
} catch (error) {
next('/error')
}
},
// 路由更新时
beforeRouteUpdate(to, from) {
this.id = to.params.id
},
// 离开路由前
beforeRouteLeave(to, from, next) {
if (this.hasUnsavedChanges) {
const answer = window.confirm('有未保存的更改,确定要离开吗?')
if (answer) {
next()
} else {
next(false)
}
} else {
next()
}
}
}4. 样式与动画:让应用更生动
组件样式隔离
vue
<style scoped>
/* scoped样式只作用于当前组件 */
.button {
background: blue;
color: white;
}
</style>
<style>
/* 全局样式 */
.global-style {
font-size: 16px;
}
</style>
<style scoped>
/* 深度选择器 - 修改子组件样式 */
.container :deep(.child-component) {
color: red;
}
/* Vue 2中的深度选择器 */
.container ::v-deep .child-component {
color: red;
}
</style>过渡与动画
vue
<template>
<div>
<!-- 基础过渡 -->
<button @click="show = !show">切换显示</button>
<transition name="fade">
<p v-if="show">Hello Vue!</p>
</transition>
<!-- 列表过渡 -->
<transition-group name="list" tag="ul">
<li v-for="item in items" :key="item.id">
{{ item.text }}
</li>
</transition-group>
</div>
</template>
<script>
export default {
data() {
return {
show: true,
items: [
{ id: 1, text: '项目1' },
{ id: 2, text: '项目2' },
{ id: 3, text: '项目3' }
]
}
}
}
</script>
<style scoped>
/* 淡入淡出过渡 */
.fade-enter-active, .fade-leave-active {
transition: opacity 0.5s;
}
.fade-enter-from, .fade-leave-to {
opacity: 0;
}
/* 列表过渡 */
.list-enter-active, .list-leave-active {
transition: all 0.5s;
}
.list-enter-from, .list-leave-to {
opacity: 0;
transform: translateX(30px);
}
.list-leave-active {
position: absolute;
}
</style>5. 实践项目:动手做一做
多页面应用
javascript
// router/index.js
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue')
},
{
path: '/products',
name: 'Products',
component: () => import('@/views/Products.vue')
},
{
path: '/cart',
name: 'Cart',
component: () => import('@/views/Cart.vue'),
meta: { requiresAuth: true }
},
{
path: '/profile',
name: 'Profile',
component: () => import('@/views/Profile.vue'),
meta: { requiresAuth: true }
}
]
const router = createRouter({
history: createWebHistory(),
routes
})
// 路由守卫
router.beforeEach((to, from, next) => {
if (to.meta.requiresAuth && !localStorage.getItem('token')) {
next('/login')
} else {
next()
}
})
export default router带状态管理的购物车
javascript
// store/cart.js (Pinia)
import { defineStore } from 'pinia'
export const useCartStore = defineStore('cart', {
state: () => ({
items: [],
total: 0
}),
getters: {
itemCount: (state) => state.items.length,
totalPrice: (state) => state.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
actions: {
addItem(product) {
const existingItem = this.items.find(item => item.id === product.id)
if (existingItem) {
existingItem.quantity++
} else {
this.items.push({ ...product, quantity: 1 })
}
this.calculateTotal()
},
removeItem(productId) {
this.items = this.items.filter(item => item.id !== productId)
this.calculateTotal()
},
updateQuantity(productId, quantity) {
const item = this.items.find(item => item.id === productId)
if (item) {
item.quantity = quantity
if (item.quantity <= 0) {
this.removeItem(productId)
}
}
this.calculateTotal()
},
calculateTotal() {
this.total = this.items.reduce((sum, item) => sum + item.price * item.quantity, 0)
},
clearCart() {
this.items = []
this.total = 0
}
}
})vue
<!-- ShoppingCart.vue -->
<template>
<div class="shopping-cart">
<h2>购物车 ({{ cart.itemCount }} 件商品)</h2>
<div v-if="cart.items.length === 0" class="empty-cart">
购物车为空
</div>
<div v-else>
<div v-for="item in cart.items" :key="item.id" class="cart-item">
<img :src="item.image" :alt="item.name" class="item-image">
<div class="item-details">
<h3>{{ item.name }}</h3>
<p>单价: ¥{{ item.price }}</p>
</div>
<div class="item-quantity">
<button @click="decreaseQuantity(item.id)">-</button>
<span>{{ item.quantity }}</span>
<button @click="increaseQuantity(item.id)">+</button>
</div>
<div class="item-total">
¥{{ (item.price * item.quantity).toFixed(2) }}
</div>
<button @click="removeItem(item.id)" class="remove-btn">删除</button>
</div>
<div class="cart-summary">
<p>总计: ¥{{ cart.totalPrice.toFixed(2) }}</p>
<button @click="checkout" class="checkout-btn">结算</button>
</div>
</div>
</div>
</template>
<script setup>
import { useCartStore } from '@/store/cart'
const cart = useCartStore()
const increaseQuantity = (productId) => {
cart.updateQuantity(productId, cart.items.find(item => item.id === productId).quantity + 1)
}
const decreaseQuantity = (productId) => {
cart.updateQuantity(productId, cart.items.find(item => item.id === productId).quantity - 1)
}
const removeItem = (productId) => {
cart.removeItem(productId)
}
const checkout = () => {
// 结算逻辑
console.log('结算订单:', cart.items)
}
</script>带动画效果的组件库
vue
<!-- AnimatedButton.vue -->
<template>
<transition name="button" @enter="handleEnter" @leave="handleLeave">
<button
v-if="visible"
:class="['animated-button', `btn-${type}`, { 'loading': loading }]"
@click="handleClick"
:disabled="disabled || loading"
>
<span v-if="loading" class="spinner"></span>
<slot v-else></slot>
</button>
</transition>
</template>
<script setup>
import { ref } from 'vue'
const props = defineProps({
type: {
type: String,
default: 'primary'
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['click'])
const visible = ref(true)
const handleClick = () => {
if (!props.disabled && !props.loading) {
emit('click')
}
}
const handleEnter = (el) => {
el.style.transform = 'scale(0)'
setTimeout(() => {
el.style.transform = 'scale(1)'
}, 10)
}
const handleLeave = (el) => {
el.style.transform = 'scale(1)'
setTimeout(() => {
el.style.transform = 'scale(0)'
}, 10)
}
</script>
<style scoped>
.animated-button {
padding: 12px 24px;
border: none;
border-radius: 6px;
font-size: 16px;
font-weight: 500;
cursor: pointer;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.btn-primary {
background: linear-gradient(45deg, #409eff, #64b5f6);
color: white;
}
.btn-success {
background: linear-gradient(45deg, #67c23a, #85ce61);
color: white;
}
.btn-danger {
background: linear-gradient(45deg, #f56c6c, #f78989);
color: white;
}
.animated-button:hover:not(:disabled) {
transform: translateY(-2px);
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
}
.animated-button:disabled {
opacity: 0.6;
cursor: not-allowed;
}
.spinner {
display: inline-block;
width: 20px;
height: 20px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
/* 按钮动画 */
.button-enter-active, .button-leave-active {
transition: all 0.3s ease;
}
.button-enter-from, .button-leave-to {
opacity: 0;
transform: scale(0.8);
}
</style>总结
本章节介绍了Vue的核心特性和进阶用法:
- 组件生命周期和高级插槽用法
- 状态管理(Vuex和Pinia)
- 路由管理(Vue Router)
- 样式隔离和动画效果
- 实际项目中的应用示例
掌握这些特性后,你可以构建更复杂、更专业的Vue应用。在下一章节中,我们将学习Vue 3的新特性和生态系统工具。
记住,Vue的强大之处在于其渐进式的特性,你可以根据项目需求选择合适的功能。多实践这些高级特性,你的Vue开发技能将更上一层楼!