Nuxt.js组件通信 #

一、组件通信概述 #

在 Vue 应用中,组件间的通信是构建复杂应用的关键。Nuxt.js 继承了 Vue 的所有通信方式,并提供了一些额外的工具。

1.1 通信方式一览 #

方式 方向 适用场景
Props 父→子 简单数据传递
Events 子→父 子组件通知父组件
v-model 双向 表单控件
Provide/Inject 跨层级 深层嵌套组件
状态管理 全局 复杂应用状态
组合式函数 共享 复用逻辑

二、Props与Events #

2.1 Props传递数据 #

父组件:

vue
<template>
  <ChildComponent
    :title="pageTitle"
    :items="listItems"
    :config="componentConfig"
  />
</template>

<script setup lang="ts">
const pageTitle = ref('页面标题')
const listItems = ref(['item1', 'item2'])
const componentConfig = reactive({
  theme: 'dark',
  size: 'large'
})
</script>

子组件:

vue
<script setup lang="ts">
interface Props {
  title: string
  items: string[]
  config: {
    theme: string
    size: string
  }
}

defineProps<Props>()
</script>

2.2 Events传递事件 #

子组件:

vue
<script setup lang="ts">
interface Emits {
  (e: 'update', value: string): void
  (e: 'delete', id: number): void
}

const emit = defineEmits<Emits>()

const handleUpdate = () => {
  emit('update', 'new value')
}

const handleDelete = (id: number) => {
  emit('delete', id)
}
</script>

父组件:

vue
<template>
  <ChildComponent
    @update="handleUpdate"
    @delete="handleDelete"
  />
</template>

<script setup lang="ts">
const handleUpdate = (value: string) => {
  console.log('更新:', value)
}

const handleDelete = (id: number) => {
  console.log('删除:', id)
}
</script>

2.3 v-model双向绑定 #

子组件:

vue
<script setup lang="ts">
interface Props {
  modelValue: string
}

interface Emits {
  (e: 'update:modelValue', value: string): void
}

defineProps<Props>()
const emit = defineEmits<Emits>()

const updateValue = (event: Event) => {
  emit('update:modelValue', (event.target as HTMLInputElement).value)
}
</script>

<template>
  <input
    :value="modelValue"
    @input="updateValue"
  />
</template>

父组件:

vue
<template>
  <CustomInput v-model="inputValue" />
</template>

<script setup lang="ts">
const inputValue = ref('')
</script>

三、Provide/Inject #

3.1 基本用法 #

祖先组件:

vue
<script setup lang="ts">
const theme = ref('dark')
const user = reactive({
  name: '张三',
  role: 'admin'
})

provide('theme', theme)
provide('user', user)
</script>

后代组件:

vue
<script setup lang="ts">
const theme = inject('theme')
const user = inject('user')

console.log(theme.value)
console.log(user.name)
</script>

3.2 类型安全的Provide/Inject #

typescript
import type { InjectionKey } from 'vue'

interface ThemeConfig {
  mode: 'light' | 'dark'
  primaryColor: string
}

const ThemeKey: InjectionKey<ThemeConfig> = Symbol('theme')

provide(ThemeKey, {
  mode: 'dark',
  primaryColor: '#3498db'
})
vue
<script setup lang="ts">
const theme = inject(ThemeKey)

if (theme) {
  console.log(theme.mode)
}
</script>

3.3 响应式Provide #

vue
<script setup lang="ts">
const state = reactive({
  count: 0,
  increment() {
    this.count++
  }
})

provide('state', readonly(state))
provide('actions', {
  increment: state.increment
})
</script>

四、组合式函数共享状态 #

4.1 共享状态模式 #

composables/useCounter.ts

typescript
const globalCount = ref(0)

export const useCounter = () => {
  const increment = () => globalCount.value++
  const decrement = () => globalCount.value--
  
  return {
    count: globalCount,
    increment,
    decrement
  }
}

组件A:

vue
<script setup lang="ts">
const { count, increment } = useCounter()
</script>

组件B:

vue
<script setup lang="ts">
const { count, decrement } = useCounter()
</script>

4.2 认证状态共享 #

composables/useAuth.ts

typescript
interface User {
  id: number
  name: string
  email: string
  role: string
}

const user = useState<User | null>('user', () => null)
const token = useCookie('token')

export const useAuth = () => {
  const isAuthenticated = computed(() => !!user.value && !!token.value)
  
  const login = async (credentials: { email: string; password: string }) => {
    const { data } = await useFetch('/api/auth/login', {
      method: 'POST',
      body: credentials
    })
    
    if (data.value) {
      user.value = data.value.user
      token.value = data.value.token
    }
  }
  
  const logout = async () => {
    await useFetch('/api/auth/logout')
    user.value = null
    token.value = null
  }
  
  const hasRole = (role: string) => {
    return user.value?.role === role
  }
  
  return {
    user,
    token,
    isAuthenticated,
    login,
    logout,
    hasRole
  }
}

五、事件总线 #

5.1 使用mitt创建事件总线 #

bash
pnpm add mitt

composables/useEventBus.ts

typescript
import mitt from 'mitt'

const emitter = mitt()

export const useEventBus = () => {
  return {
    on: emitter.on,
    off: emitter.off,
    emit: emitter.emit
  }
}

5.2 使用事件总线 #

组件A(发送事件):

vue
<script setup lang="ts">
const bus = useEventBus()

const sendMessage = () => {
  bus.emit('message', { text: 'Hello!' })
}
</script>

组件B(接收事件):

vue
<script setup lang="ts">
const bus = useEventBus()
const message = ref('')

onMounted(() => {
  bus.on('message', (data: any) => {
    message.value = data.text
  })
})

onUnmounted(() => {
  bus.off('message')
})
</script>

六、组件引用 #

6.1 使用ref获取组件实例 #

父组件:

vue
<template>
  <ChildComponent ref="childRef" />
  <button @click="callChildMethod">调用子组件方法</button>
</template>

<script setup lang="ts">
const childRef = ref()

const callChildMethod = () => {
  childRef.value?.doSomething()
}
</script>

子组件:

vue
<script setup lang="ts">
const doSomething = () => {
  console.log('子组件方法被调用')
}

defineExpose({
  doSomething
})
</script>

6.2 模板引用与组合式函数 #

vue
<script setup lang="ts">
const inputRef = ref<HTMLInputElement>()

const focus = () => {
  inputRef.value?.focus()
}

defineExpose({
  focus
})
</script>

<template>
  <input ref="inputRef" />
</template>

七、父子组件完整示例 #

7.1 父组件 #

vue
<template>
  <div class="parent">
    <h1>用户管理</h1>
    
    <UserForm
      v-model="newUser"
      @submit="addUser"
    />
    
    <UserList
      :users="users"
      @edit="editUser"
      @delete="deleteUser"
    >
      <template #empty>
        <p>暂无用户数据</p>
      </template>
    </UserList>
  </div>
</template>

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

const users = ref<User[]>([])
const newUser = ref({ name: '', email: '' })

const addUser = () => {
  users.value.push({
    id: Date.now(),
    ...newUser.value
  })
  newUser.value = { name: '', email: '' }
}

const editUser = (user: User) => {
  console.log('编辑用户:', user)
}

const deleteUser = (id: number) => {
  users.value = users.value.filter(u => u.id !== id)
}
</script>

7.2 子组件 - UserForm #

vue
<template>
  <form @submit.prevent="$emit('submit')">
    <div>
      <label>姓名</label>
      <input v-model="localUser.name" />
    </div>
    <div>
      <label>邮箱</label>
      <input v-model="localUser.email" type="email" />
    </div>
    <button type="submit">添加</button>
  </form>
</template>

<script setup lang="ts">
interface User {
  name: string
  email: string
}

interface Props {
  modelValue: User
}

interface Emits {
  (e: 'update:modelValue', value: User): void
  (e: 'submit'): void
}

const props = defineProps<Props>()
const emit = defineEmits<Emits>()

const localUser = computed({
  get: () => props.modelValue,
  set: (value) => emit('update:modelValue', value)
})
</script>

7.3 子组件 - UserList #

vue
<template>
  <div class="user-list">
    <slot v-if="users.length === 0" name="empty" />
    
    <ul v-else>
      <li v-for="user in users" :key="user.id">
        <span>{{ user.name }} - {{ user.email }}</span>
        <button @click="$emit('edit', user)">编辑</button>
        <button @click="$emit('delete', user.id)">删除</button>
      </li>
    </ul>
  </div>
</template>

<script setup lang="ts">
interface User {
  id: number
  name: string
  email: string
}

defineProps<{
  users: User[]
}>()

defineEmits<{
  (e: 'edit', user: User): void
  (e: 'delete', id: number): void
}>()
</script>

八、跨层级组件通信 #

8.1 使用Provide/Inject #

祖先组件:

vue
<script setup lang="ts">
const selectedUser = ref<User | null>(null)
const selectUser = (user: User) => {
  selectedUser.value = user
}

provide('userContext', {
  selected: selectedUser,
  select: selectUser
})
</script>

后代组件:

vue
<script setup lang="ts">
const { selected, select } = inject('userContext')
</script>

<template>
  <div @click="select(user)">
    {{ user.name }}
  </div>
</template>

8.2 使用组合式函数 #

composables/useUserSelection.ts

typescript
const selectedUser = ref<User | null>(null)

export const useUserSelection = () => {
  const select = (user: User) => {
    selectedUser.value = user
  }
  
  const clear = () => {
    selectedUser.value = null
  }
  
  return {
    selected: selectedUser,
    select,
    clear
  }
}

九、最佳实践 #

9.1 选择合适的通信方式 #

场景 推荐方式
父子简单数据 Props
子通知父 Events
表单双向绑定 v-model
深层嵌套 Provide/Inject
全局状态 useState/Pinia
跨组件事件 事件总线

9.2 避免过度使用 #

  • 不要滥用全局状态
  • 保持组件职责单一
  • 优先使用 Props/Events
  • 组合式函数优于混入

十、总结 #

本章介绍了 Nuxt.js 组件通信:

  • Props 和 Events 实现父子通信
  • v-model 实现双向绑定
  • Provide/Inject 实现跨层级通信
  • 组合式函数共享状态
  • 事件总线实现任意组件通信
  • 组件引用调用子组件方法

合理选择通信方式是构建可维护应用的关键,下一章我们将学习元数据与 SEO。

最后更新:2026-03-28