组件通信 | Vue Router | Vuex/Pinia | Vue3 Composition API

35 阅读13分钟

一、Vue 3 Composition API

Vue 3 Composition API 是 Vue 3 核心的语法升级,核心目标是解决 Vue 2 Options API 在复杂组件中 “代码碎片化” 的问题,同时提升逻辑复用性、类型推导友好性。相比于 Options API(data、methods、computed、watch 等选项式写法),Composition API 以函数式的方式组织代码,让相关逻辑内聚,而非分散在不同选项中。

1. 核心设计理念

在 Vue 2 中,一个处理表单验证、数据请求、状态监听的复杂组件,其验证逻辑可能分散在 methodswatchcomputed 中,代码跳转成本高;而 Composition API 允许将 “表单验证逻辑”“数据请求逻辑” 分别封装成独立函数,再在 setup 中组合,实现 “按逻辑维度组织代码”,而非 “按 API 类型组织代码”。同时,Composition API 天然适配 TypeScript,能提供更精准的类型推导,解决了 Vue 2 中 this 指向模糊、类型提示不友好的问题。

2. 核心语法与使用场景

(1)setup 函数:入口点

setup 是 Composition API 的核心入口,组件创建前执行(在 beforeCreate 钩子前),此时组件实例尚未创建,this 指向 undefined。它接收两个参数:props(组件传入的属性,响应式且不可解构)、context(包含 attrs、slots、emit 等上下文),返回值可直接在模板中使用。

<template>
  <div>{{ count }} {{ doubleCount }}</div>
</template>
<script>
import { ref, computed } from 'vue'
export default {
  setup() {
    // 基础响应式数据
    const count = ref(0)
    // 计算属性
    const doubleCount = computed(() => count.value * 2)
    // 方法
    const increment = () => count.value++
    // 返回给模板
    return { count, doubleCount, increment }
  }
}
</script>

Vue 3 中 <script setup> 语法的简写形式,以下是 Vue 3 推荐的更简洁的写法,无需手动导出 setup 函数,代码量更少且更直观。以下是完整的简写版本:

<template>
  <div>{{ count }} {{ doubleCount }}</div>
  <!-- 可直接调用 increment 方法,比如加个按钮测试 -->
  <button @click="increment">+1</button>
</template>

<!-- 使用 setup 语法糖,直接在 script 内写逻辑 -->
<script setup>
import { ref, computed } from 'vue'

// 基础响应式数据(模板中自动可用,无需 return)
const count = ref(0)

// 计算属性
const doubleCount = computed(() => count.value * 2)

// 方法(模板中自动可用)
const increment = () => count.value++
</script>

核心简化点说明

  1. <script setup> 是语法糖,无需定义 export defaultsetup 函数,直接在 script 内编写逻辑;
  2. 声明的变量(countdoubleCount)和方法(increment)无需手动 return,会自动暴露给模板使用;
  3. 核心 API(refcomputed)的使用逻辑完全不变,仅简化了代码结构;
  4. 这是 Vue 3 项目中最主流、最推荐的写法,相比原写法更简洁且易维护。

(2)响应式 API:ref 与 reactive

  • ref:用于创建基本类型(字符串、数字、布尔)的响应式数据,内部通过包裹对象实现响应式,访问 / 修改需通过 .value(模板中自动解包);
  • reactive:用于创建对象 / 数组类型的响应式数据,基于 ES6 Proxy 实现(相比 Vue 2 的 Object.defineProperty 支持数组、新增属性),直接操作属性即可,无需 .value
import { ref, reactive } from 'vue'
// 基本类型响应式
const name = ref('张三')
name.value = '李四' // 修改
// 对象类型响应式
const user = reactive({ age: 20 })
user.age = 21 // 修改
// 解构响应式对象(需用 toRefs 保持响应式)
import { toRefs } from 'vue'
const { age } = toRefs(user)

(3)生命周期钩子:组合式写法

Vue 2 的生命周期钩子在 Composition API 中以 onXXX 形式存在,需从 vue 导入,且只能在 setup 中调用:

Vue 2 钩子Composition API
createdsetup 内部直接写
mountedonMounted
updatedonUpdated
unmountedonUnmounted
import { onMounted, onUnmounted } from 'vue'
setup() {
  onMounted(() => {
    console.log('组件挂载')
  })
  onUnmounted(() => {
    console.log('组件卸载')
  })
}

(4)逻辑复用:自定义组合式函数

这是 Composition API 最核心的优势之一。将通用逻辑封装成以 use 开头的函数,实现跨组件复用,替代 Vue 2 的 mixins(mixins 存在命名冲突、来源模糊问题)。

// useFetch.js(封装数据请求逻辑)
import { ref, onMounted } from 'vue'
export function useFetch(url) {
  const data = ref(null)
  const loading = ref(true)
  const error = ref(null)

  const fetchData = async () => {
    try {
      const res = await fetch(url)
      data.value = await res.json()
    } catch (err) {
      error.value = err
    } finally {
      loading.value = false
    }
  }

  onMounted(fetchData)
  return { data, loading, error }
}

// 组件中使用
import { useFetch } from './useFetch'
setup() {
  const { data, loading, error } = useFetch('/api/user')
  return { data, loading, error }
}

3. 优势与适用场景

  • 适合复杂组件:将分散的逻辑聚合,提升可读性和维护性;
  • 逻辑复用更优雅:自定义组合式函数替代 mixins、高阶组件,逻辑来源清晰;
  • TypeScript 友好:通过类型注解实现精准类型推导;
  • 按需导入:减小打包体积(仅导入使用的 API)。

二、Vuex/Pinia 状态管理

在 Vue 应用中,组件间共享状态(如用户信息、全局配置、购物车数据)需要统一的状态管理方案,Vuex(Vue 2 主流)和 Pinia(Vue 3 官方推荐)是核心解决方案,Pinia 可视为 Vuex 5 的替代方案,更轻量、更贴合 Composition API。

image.png

1. Vuex:传统状态管理方案

Vuex 基于 “单向数据流” 设计,核心概念包括 State、Getter、Mutation、Action、Module,适用于 Vue 2 或 Vue 3 兼容场景。

(1)核心结构

// store/index.js
import Vue from 'vue'
import Vuex from 'vuex'
Vue.use(Vuex)

export default new Vuex.Store({
  // 全局状态 🤔🤔🤔
  state: {
    count: 0,
    user: { name: '张三' }
  },
  // 计算派生状态(类似组件 computed) 🤔🤔🤔
  getters: {
    doubleCount: state => state.count * 2,
    userName: state => state.user.name
  },
  // 同步修改状态(唯一方式) 🤔🤔🤔
  mutations: {
    increment(state, payload) {
      state.count += payload
    },
    updateUser(state, name) {
      state.user.name = name
    }
  },
  // 异步操作(可提交 mutation) 🤔🤔🤔
  actions: {
    async incrementAsync({ commit }, payload) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      commit('increment', payload)
    }
  },
  // 模块拆分(解决状态臃肿) 🤔🤔🤔
  modules: {
    cart: {
      namespaced: true, // 命名空间,避免命名冲突
      state: { goods: [] },
      mutations: { addGoods(state, goods) { state.goods.push(goods) } }
    }
  }
})

(2)组件中使用

<template>
  <div>{{ $store.state.count }} {{ doubleCount }}</div>
</template>
<script>
import { mapState, mapGetters, mapMutations, mapActions } from 'vuex'
export default {
  computed: {
    // 映射 state 🤔🤔🤔
    ...mapState(['count']),
    // 映射 getter 🤔🤔🤔
    ...mapGetters(['doubleCount'])
  },
  methods: {
    // 映射 mutation 🤔🤔🤔
    ...mapMutations(['increment']),
    // 映射 action 🤔🤔🤔
    ...mapActions(['incrementAsync']),
    handleClick() {
      this.increment(1) // 调用 mutation
      this.incrementAsync(2) // 调用 action
      // 模块内调用(命名空间)
      this.$store.commit('cart/addGoods', { id: 1, name: '商品' })
    }
  }
}
</script>

(3)Vuex 的局限性

  • 类型支持差:与 TypeScript 结合需额外配置,类型推导不友好;
  • 模块化繁琐:命名空间、模块嵌套增加复杂度;
  • 代码冗余:mutation/action 分离,简单场景下写法繁琐;
  • Vue 3 适配性:需安装 vuex@4,且与 Composition API 结合时需手动适配。

2. Pinia:Vue 3 官方推荐方案

Pinia 解决了 Vuex 的痛点,去掉了 Mutation(直接在 Action 中修改状态),天然支持 TypeScript,体积更小(约 1KB),且无需嵌套模块,采用扁平化设计。

(1)核心优势

  • 无 Mutation:Action 支持同步 / 异步修改状态,简化代码;
  • 类型安全:无需额外配置,TypeScript 自动推导类型;
  • 模块化更简单:每个 Store 是独立模块,无需命名空间;
  • 适配 Composition API:提供 useStore 钩子,无缝集成;
  • 轻量:无依赖,打包体积远小于 Vuex。

(2)基本使用

第一步:安装并注册

npm install pinia
// main.js(Vue 3)
import { createApp } from 'vue'
import { createPinia } from 'pinia'
import App from './App.vue'

const app = createApp(App)
app.use(createPinia())
app.mount('#app')

第二步:定义 Store

// store/counter.js
import { defineStore } from 'pinia'

// 定义并导出 Store(id 唯一,用于 devtools 标识)
export const useCounterStore = defineStore('counter', {
  // 状态(类似 Vuex state) 🤔🤔🤔
  state: () => ({
    count: 0,
    user: { name: '张三' }
  }),
  // 派生状态(类似 Vuex getter) 🤔🤔🤔
  getters: {
    doubleCount: (state) => state.count * 2,
    // 访问其他 getter 或 state
    doubleCountPlusOne() {
      return this.doubleCount + 1
    }
  },
  // 操作(同步/异步,类似 Vuex action) 🤔🤔🤔
  actions: {
    increment(payload) {
      this.count += payload // 直接修改状态
    },
    async incrementAsync(payload) {
      await new Promise(resolve => setTimeout(resolve, 1000))
      this.increment(payload)
    },
    updateUser(name) {
      this.user.name = name
    }
  }
})

第三步:组件中使用

<template>
  <div>{{ counterStore.count }} {{ counterStore.doubleCount }}</div>
</template>

<script setup>
import { useCounterStore } from '@/store/counter'
// 获取 Store 实例(响应式,无需手动解包)
const counterStore = useCounterStore()

// 修改状态
const handleIncrement = () => {
  counterStore.increment(1)
  counterStore.incrementAsync(2)
}

// 批量修改状态($patch 优化性能)
const batchUpdate = () => {
  counterStore.$patch({
    count: 10,
    user: { name: '李四' }
  })
  // 或函数式(适合复杂修改)
  counterStore.$patch((state) => {
    state.count += 5
    state.user.name = '王五'
  })
}
</script>

(3)Store 间通信

// store/cart.js
import { useUserStore } from './user'
export const useCartStore = defineStore('cart', {
  actions: {
    addGoods(goods) {
      const userStore = useUserStore()
      // 实现 用户和购物车 之间通信
      if (userStore.isLogin) {
        this.goodsList.push(goods)
      }
    }
  }
})

3. 选型建议

  • Vue 2 项目:继续使用 Vuex 3,无需迁移;
  • Vue 3 新项目:优先使用 Pinia,简化开发、提升类型体验;
  • 大型 Vue 3 项目:Pinia 更适合,模块化清晰、维护成本低。

三、Vue Router

Vue Router 是 Vue 官方的路由管理器,用于实现单页面应用(SPA)的路由跳转,核心作用是将 URL 路径与组件映射,实现无刷新页面切换,适配 Vue 2/3 版本(Vue Router 3 对应 Vue 2,Vue Router 4 对应 Vue 3)。

1. 核心概念

SPA 本质是一个 HTML 文件,路由通过监听 URL 变化(hash 模式 /history 模式),动态替换页面内容,Vue Router 封装了这一过程,核心概念包括:

  • 路由实例:通过 createRouter 创建,配置路由规则、模式等;
  • 路由规则:定义路径与组件的映射关系;
  • 路由出口:<router-view>,渲染匹配的组件;
  • 路由导航:<router-link>(替代 a 标签)或编程式导航(router.push);
  • 路由守卫:拦截路由跳转,实现权限控制、页面跳转前验证等。

2. Vue Router 4(Vue 3 版本)核心使用

(1)安装与配置

npm install vue-router@4
// router/index.js
import { createRouter, createWebHistory, createWebHashHistory } from 'vue-router'
import Home from '@/views/Home.vue'
import About from '@/views/About.vue'

// 路由规则
const routes = [
  {
    path: '/', // 根路径
    name: 'Home', // 路由命名
    component: Home, // 匹配的组件
    meta: { requiresAuth: false } // 自定义元信息(用于守卫)
  },
  {
    path: '/about',
    name: 'About',
    component: () => import('@/views/About.vue') // 懒加载(按需加载组件)
  },
  {
    path: '/user/:id', // 动态路由参数
    name: 'User',
    component: () => import('@/views/User.vue'),
    props: true // 将路由参数作为 props 传入组件
  },
  {
    path: '/:pathMatch(.*)*', // 404 路由(匹配所有未定义路径)
    component: () => import('@/views/404.vue')
  }
]

// 创建路由实例
const router = createRouter({
  history: createWebHistory(), // history 模式(需后端配置)
  // history: createWebHashHistory(), // hash 模式(无需后端配置)
  routes
})

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')

(2)模板中使用路由

<!-- App.vue -->
<template>
  <div>
    <!-- 路由导航(替代 a 标签) -->
    <router-link to="/">首页</router-link>
    <router-link :to="{ name: 'About' }">关于</router-link>
    <router-link :to="{ name: 'User', params: { id: 1 } }">用户1</router-link>

    <!-- 路由出口:渲染匹配的组件 -->
    <router-view />
  </div>
</template>

(3)编程式导航

在组件中通过 useRouter/useRoute 钩子访问路由实例和当前路由信息:

<script setup>
import { useRouter, useRoute } from 'vue-router'

const router = useRouter() // 路由实例(用于导航)
const route = useRoute() // 当前路由信息(包含 params、query 等)

// 跳转路由
const goToAbout = () => {
  // 方式1:路径跳转
  router.push('/about')
  // 方式2:命名路由(推荐,修改路径无需改代码)
  router.push({ name: 'About' })
  // 带查询参数:/user/1?name=张三
  router.push({ name: 'User', params: { id: 1 }, query: { name: '张三' } })
}

// 替换当前路由(无历史记录)
const replaceToHome = () => {
  router.replace('/')
}

// 前进/后退
const goBack = () => {
  router.go(-1) // 后退一步
}

// 获取路由参数
console.log(route.params.id) // 动态参数
console.log(route.query.name) // 查询参数
</script>

(4)路由守卫

路由守卫用于控制路由跳转权限,分为全局守卫、路由独享守卫、组件内守卫。

全局守卫(全局生效)
// router/index.js
// 全局前置守卫(跳转前触发)
router.beforeEach((to, from, next) => {
  // to:目标路由;from:当前路由;next:继续跳转的函数
  // 验证是否需要登录
  if (to.meta.requiresAuth && !localStorage.getItem('token')) {
    next('/login') // 跳转到登录页
  } else {
    next() // 正常跳转
  }
})

// 全局后置守卫(跳转后触发,无 next)
router.afterEach((to, from) => {
  // 记录页面访问日志、修改页面标题等
  document.title = to.name || '默认标题'
})
路由独享守卫(仅当前路由生效)
const routes = [
  {
    path: '/admin',
    component: () => import('@/views/Admin.vue'),
    beforeEnter: (to, from, next) => {
      // 仅/admin 路由生效的守卫
      if (!localStorage.getItem('isAdmin')) {
        next('/403')
      } else {
        next()
      }
    }
  }
]
组件内守卫(仅当前组件生效)
<script setup>
import { onBeforeRouteEnter, onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'

// 进入组件前触发(此时组件实例未创建,无法访问 this)
onBeforeRouteEnter((to, from, next) => {
  next(vm => {
    // vm 是组件实例,可访问组件数据
    console.log(vm.count)
  })
})

// 路由参数更新时触发(如 /user/1 → /user/2)
onBeforeRouteUpdate((to, from, next) => {
  console.log('参数更新:', to.params.id)
  next()
})

// 离开组件时触发(如确认是否离开表单页面)
onBeforeRouteLeave((to, from, next) => {
  const confirm = window.confirm('是否确认离开?')
  if (confirm) {
    next()
  } else {
    next(false) // 取消跳转
  }
})
</script>

(5)嵌套路由

用于实现页面布局嵌套(如侧边栏 + 主内容):

// 路由规则
const routes = [
  {
    path: '/admin',
    component: () => import('@/views/AdminLayout.vue'),
    children: [
      { path: '', component: () => import('@/views/AdminHome.vue') }, // 子路由默认页
      { path: 'user', component: () => import('@/views/AdminUser.vue') }, // /admin/user
      { path: 'order', component: () => import('@/views/AdminOrder.vue') } // /admin/order
    ]
  }
]
<!-- AdminLayout.vue -->
<template>
  <div class="admin-layout">
    <div class="sidebar">
      <router-link to="/admin/user">用户管理</router-link>
      <router-link to="/admin/order">订单管理</router-link>
    </div>
    <div class="main">
      <router-view /> <!-- 渲染子路由组件 -->
    </div>
  </div>
</template>

3. 关键注意事项

  • history 模式需后端配置:Nginx/Apache 需配置所有路径指向 index.html,避免刷新 404;
  • 懒加载优化性能:通过动态 import 拆分代码包,减小首屏加载体积;
  • 路由参数解耦:使用 props: true 将参数传入组件,避免组件依赖 $route
  • 权限控制:优先使用全局前置守卫统一处理登录 / 权限验证。

四、组件通信

组件通信是 Vue 开发的核心基础能力,所有基于组件化拆分的 Vue 应用,都需要通过合理的通信方式实现数据传递、事件触发与状态共享。Vue 围绕 “单向数据流” 核心原则,提供了多套通信方案,适配父子、兄弟、跨层级等不同组件关系场景。理解各类通信方式的原理、适用场景与最佳实践,是保障代码可维护性、数据流可追溯的关键。

1 父子组件通信(核心常用场景)

父子组件是最直接的组件关系,也是通信需求最频繁的场景,Vue 为其设计了一套清晰、规范的通信体系,核心遵循 “父传子靠 Props,子传父靠自定义事件” 的原则。

1.1 父传子:Props 传递数据

Props 是父组件向子组件传递数据的唯一官方推荐方式,本质是子组件对外暴露的 “数据输入口”。Props 具有只读特性,子组件不可直接修改父组件传递的 Props,否则会触发 Vue 警告,这是为了保证单向数据流的清晰性。

在 Vue 3 的 <script setup> 语法中,通过 defineProps 声明接收的 Props,支持类型验证、默认值、必填项等配置,能在开发阶段提前捕获数据类型错误:

<!-- 子组件 -->
<script setup>
// 带类型验证的 Props 声明
const props = defineProps({
  // 基本类型
  title: {
    type: String,
    default: '默认标题'
  },
  // 对象类型(默认值需用函数返回,避免引用共享)
  user: {
    type: Object,
    required: true,
    default: () => ({ name: '未知', age: 0 })
  }
})
</script>

父组件只需通过属性绑定的方式传递数据,即可完成 “父传子”:

<!-- 父组件 -->
<template>
  <Child :title="pageTitle" :user="userInfo" />
</template>
<script setup>
import { ref, reactive } from 'vue'
const pageTitle = ref('用户详情')
const userInfo = reactive({ name: '张三', age: 25 })
</script>

1.2 子传父:自定义事件

子组件无法直接修改父组件数据,需通过 “触发自定义事件” 的方式通知父组件,由父组件自行修改数据。Vue 3 中通过 defineEmits 声明组件可触发的事件,再通过 emit 方法触发并传递参数。

<!-- 子组件 -->
<script setup>
// 声明可触发的事件
const emit = defineEmits(['update-user', 'submit'])

const handleClick = () => {
  // 触发事件并传递参数
  emit('update-user', { name: '李四', age: 26 })
  emit('submit')
}
</script>

父组件通过 @事件名 监听子组件事件,在回调中处理数据更新:

<!-- 父组件 -->
<template>
  <Child @update-user="handleUpdateUser" @submit="handleSubmit" />
</template>
<script setup>
const handleUpdateUser = (newUser) => {
  // 父组件修改自身数据
  Object.assign(userInfo, newUser)
}
const handleSubmit = () => {
  console.log('子组件触发了提交事件')
}
</script>

1.3 特殊场景:ref + defineExpose

若父组件需要直接访问子组件的属性或调用子组件的方法,可通过 ref 获取子组件实例,子组件需通过 defineExpose 显式暴露需要对外公开的内容(Vue 3 中组件内部数据默认封闭)。

<!-- 子组件 -->
<script setup>
import { ref } from 'vue'
const count = ref(0)
const reset = () => {
  count.value = 0
}
// 暴露属性/方法给父组件
defineExpose({ count, reset })
</script>
<!-- 父组件 -->
<template>
  <Child ref="childRef" />
</template>
<script setup>
import { ref } from 'vue'
const childRef = ref(null)

// 调用子组件方法
const callChildMethod = () => {
  childRef.value.reset()
}
</script>

该方式需谨慎使用,过度依赖会增加组件耦合度,仅适用于 “父组件需主动控制子组件行为” 的特殊场景(如重置子组件表单)。

2 兄弟组件通信

兄弟组件无直接关联,需通过 “中间媒介” 实现通信,核心方案分为三类,适配不同复杂度场景。

2.1 父组件中转(简单场景首选)

以父组件作为中间层,接收一个子组件的事件数据,再通过 Props 将数据传递给另一个子组件。该方式逻辑简单、数据流清晰,适用于兄弟组件通信逻辑较简单的场景。

<!-- 父组件 -->
<template>
  <Brother1 @send-data="handleData" />
  <Brother2 :data="sharedData" />
</template>
<script setup>
import { ref } from 'vue'
const sharedData = ref('')
// 接收兄弟1的数据,传递给兄弟2
const handleData = (data) => {
  sharedData.value = data
}
</script>

2.2 全局状态管理(复杂场景首选)

对于中大型应用,推荐使用 Pinia/Vuex 作为全局状态容器,兄弟组件通过读写全局状态实现通信,无需依赖父组件中转。Pinia 作为 Vue 3 官方推荐方案,语法简洁且天然支持 TypeScript:

// store/index.js
import { defineStore } from 'pinia'
export const useSharedStore = defineStore('shared', {
  state: () => ({
    brotherData: ''
  }),
  actions: {
    updateData(data) {
      this.brotherData = data
    }
  }
})

兄弟组件只需导入并操作全局状态即可:

<!-- 兄弟1组件 -->
<script setup>
import { useSharedStore } from '@/store'
const store = useSharedStore()
// 修改全局状态
const sendData = () => {
  store.updateData('兄弟1的消息')
}
</script>
<!-- 兄弟2组件 -->
<script setup>
import { useSharedStore } from '@/store'
const store = useSharedStore()
// 读取全局状态(响应式)
console.log(store.brotherData)
</script>

2.3 事件总线(轻量临时方案)

Vue 2 中的 $bus 方式在 Vue 3 中不再适用,可通过第三方库 mitt 实现轻量级事件总线,适用于小型项目或临时通信需求:

npm install mitt
// utils/bus.js
import mitt from 'mitt'
export const bus = mitt()
<!-- 兄弟1组件 -->
<script setup>
import { bus } from '@/utils/bus'
const send = () => {
  bus.emit('msg', '兄弟通信消息')
}
</script>
<!-- 兄弟2组件 -->
<script setup>
import { onMounted, onUnmounted } from 'vue'
import { bus } from '@/utils/bus'
onMounted(() => {
  bus.on('msg', (data) => console.log(data))
})
// 组件卸载时取消监听,避免内存泄漏
onUnmounted(() => {
  bus.off('msg')
})
</script>

该方式的缺点是事件名易冲突、数据流难以追踪,中大型项目不推荐作为核心通信方式。

3 跨层级 / 任意组件通信

跨层级组件(如爷孙组件)或无关联的任意组件通信,核心方案为 Provide/Inject 和全局状态管理。

3.1 Provide/Inject(依赖注入)

Vue 提供的依赖注入 API,父组件通过 provide 提供数据 / 方法,所有后代组件(无论层级多深)都可通过 inject 注入使用,解决了 “Props 透传” 问题(中间组件无需传递无关 Props)。

<!-- 顶层父组件 -->
<script setup>
import { provide, ref } from 'vue'
const theme = ref('light')
// 提供数据
provide('theme', theme)
// 提供修改数据的方法
provide('changeTheme', (newTheme) => {
  theme.value = newTheme
})
</script>
<!-- 深层子组件 -->
<script setup>
import { inject } from 'vue'
// 注入数据和方法
const theme = inject('theme')
const changeTheme = inject('changeTheme')

// 调用方法修改顶层数据
const switchTheme = () => {
  changeTheme('dark')
}
</script>

需注意:Provide/Inject 会使组件关系变得隐式,建议仅用于 “全局配置类数据”(如主题、语言)的传递,避免滥用导致数据流混乱。

3.2 全局状态管理(通用方案)

Pinia/Vuex 是跨层级、任意组件通信的通用方案,无论组件关系如何,都可通过读写全局状态实现通信,是中大型 Vue 应用的首选。其优势在于状态集中管理、修改可追踪,且支持调试工具可视化监控状态变化。

4 通信方式选型原则

组件关系推荐方案核心优势
父子组件Props + 自定义事件数据流清晰、符合单向数据流
兄弟组件父组件中转(简单)/ Pinia(复杂)简单场景低成本,复杂场景解耦
跨层级 / 任意组件Provide/Inject(轻量)/ Pinia(通用)避免 Props 透传,状态集中可控

核心原则总结:优先选择 “最小作用域” 的通信方式,数据尽量存储在 “需要使用该数据的最顶层组件” 中;避免直接修改子组件 / 注入的外部数据,通过事件 / 方法通知数据所有者修改,保证数据流可追溯。

  1. 父子组件通信是基础,核心依赖 Props(父传子)和自定义事件(子传父),ref + defineExpose 仅用于特殊场景;
  2. 兄弟组件通信优先用父组件中转或 Pinia,事件总线仅适用于轻量临时场景;
  3. 跨层级组件通信可选择 Provide/Inject(轻量)或 Pinia(通用),遵循单向数据流原则;
  4. 中大型项目建议以 Pinia 为核心管理全局共享数据,降低组件耦合度。

最后总结 🤔

  • Vue 3 Composition API:以 setup 为核心入口,通过 ref/reactive 实现响应式,解决复杂组件逻辑碎片化问题,支持逻辑复用,适配 TypeScript。

  • Vuex/Pinia:用于全局状态管理,Pinia 作为 Vue 3 官方推荐方案,简化语法、去掉 Mutation,轻量且类型支持更优,替代传统 Vuex。

  • Vue Router:实现 SPA 路由跳转,支持路由守卫、嵌套路由等功能,管控页面跳转规则与权限验证。

  • 组件通信:按组件关系选型,父子组件用 Props + 自定义事件,兄弟 / 跨层级组件通过父组件中转、Pinia 或 Provide/Inject,遵循单向数据流原则。