Skip to content

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 router
vue
<!-- 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开发技能将更上一层楼!