基础篇三 Nuxt4 组件进阶:插槽与事件传递

文章目录

写过 Vue 的同学都知道,组件通信是个大话题。props 向下传,emit 向上发,听起来简单,但实际项目中各种复杂场景让人头大。今天深入聊聊组件间通信的进阶技巧------插槽和事件传递。

一、插槽基础

插槽让组件的内容可以由父组件决定:

vue 复制代码
<!-- components/MyCard.vue -->
<template>
  <div class="card">
    <slot />
  </div>
</template>

<style scoped>
.card {
  border: 1px solid #eee;
  border-radius: 8px;
  padding: 1rem;
}
</style>

使用时:

vue 复制代码
<MyCard>
  <h2>标题</h2>
  <p>这是卡片内容</p>
</MyCard>

二、默认内容

插槽可以设置默认内容,父组件不传时显示:

vue 复制代码
<!-- components/MyButton.vue -->
<template>
  <button class="btn">
    <slot>
      点击我  <!-- 默认内容 -->
    </slot>
  </button>
</template>
vue 复制代码
<!-- 使用默认内容 -->
<MyButton />

<!-- 覆盖默认内容 -->
<MyButton>提交</MyButton>

三、具名插槽

一个组件可能有多个区域需要填充:

vue 复制代码
<!-- components/ArticleCard.vue -->
<template>
  <article class="article-card">
    <header>
      <slot name="header" />
    </header>
    
    <main>
      <slot />  <!-- 默认插槽 -->
    </main>
    
    <footer>
      <slot name="footer" />
    </footer>
  </article>
</template>

使用:

vue 复制代码
<ArticleCard>
  <template #header>
    <h2>文章标题</h2>
  </template>
  
  <p>文章正文内容...</p>
  
  <template #footer>
    <span>2024-01-15</span>
  </template>
</ArticleCard>

四、作用域插槽

父组件想用子组件的数据怎么办?作用域插槽:

vue 复制代码
<!-- components/UserList.vue -->
<script setup lang="ts">
const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 },
  { id: 3, name: 'Charlie', age: 35 }
]
</script>

<template>
  <ul>
    <li v-for="user in users" :key="user.id">
      <slot :user="user" :index="user.id">
        {{ user.name }}  <!-- 默认渲染 -->
      </slot>
    </li>
  </ul>
</template>

父组件自定义渲染:

vue 复制代码
<UserList>
  <template #default="{ user, index }">
    <span>{{ index }}. {{ user.name }} ({{ user.age }}岁)</span>
  </template>
</UserList>

五、高阶:表格组件

作用域插槽最经典的场景是表格组件:

vue 复制代码
<!-- components/DataTable.vue -->
<script setup lang="ts">
interface Column {
  key: string
  title: string
}

const props = defineProps<{
  columns: Column[]
  data: any[]
}>()
</script>

<template>
  <table>
    <thead>
      <tr>
        <th v-for="col in columns" :key="col.key">
          {{ col.title }}
        </th>
      </tr>
    </thead>
    <tbody>
      <tr v-for="(row, index) in data" :key="index">
        <td v-for="col in columns" :key="col.key">
          <slot :name="col.key" :row="row" :value="row[col.key]">
            {{ row[col.key] }}
          </slot>
        </td>
      </tr>
    </tbody>
  </table>
</template>

使用:

vue 复制代码
<script setup lang="ts">
const columns = [
  { key: 'name', title: '姓名' },
  { key: 'age', title: '年龄' },
  { key: 'actions', title: '操作' }
]

const users = [
  { id: 1, name: 'Alice', age: 25 },
  { id: 2, name: 'Bob', age: 30 }
]
</script>

<template>
  <DataTable :columns="columns" :data="users">
    <!-- 自定义年龄列 -->
    <template #age="{ value }">
      <span :class="{ 'text-red': value > 28 }">
        {{ value }}岁
      </span>
    </template>
    
    <!-- 自定义操作列 -->
    <template #actions="{ row }">
      <button @click="edit(row)">编辑</button>
      <button @click="delete(row)">删除</button>
    </template>
  </DataTable>
</template>

六、事件传递:emit

子组件向父组件传数据,用 emit

vue 复制代码
<!-- components/Counter.vue -->
<script setup lang="ts">
const count = ref(0)

const emit = defineEmits<{
  change: [value: number]
  reset: []
}>()

const increment = () => {
  count.value++
  emit('change', count.value)
}

const reset = () => {
  count.value = 0
  emit('reset')
}
</script>

<template>
  <div>
    <p>计数: {{ count }}</p>
    <button @click="increment">+1</button>
    <button @click="reset">重置</button>
  </div>
</template>

父组件监听:

vue 复制代码
<script setup lang="ts">
const handleChange = (value: number) => {
  console.log('计数变化:', value)
}

const handleReset = () => {
  console.log('已重置')
}
</script>

<template>
  <Counter @change="handleChange" @reset="handleReset" />
</template>

七、v-model 双向绑定

v-model 本质是 :value + @update:value 的语法糖:

vue 复制代码
<!-- components/SearchInput.vue -->
<script setup lang="ts">
const props = defineProps<{
  modelValue: string
}>()

const emit = defineEmits<{
  'update:modelValue': [value: string]
}>()

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

<template>
  <input 
    :value="modelValue" 
    @input="input"
    placeholder="搜索..."
  />
</template>

使用:

vue 复制代码
<script setup lang="ts">
const keyword = ref('')
</script>

<template>
  <SearchInput v-model="keyword" />
  <p>搜索词: {{ keyword }}</p>
</template>

Vue 3.4+ 可以用 defineModel 简化:

vue 复制代码
<!-- components/SearchInput.vue -->
<script setup lang="ts">
const keyword = defineModel<string>()
</script>

<template>
  <input v-model="keyword" placeholder="搜索..." />
</template>

八、多个 v-model

一个组件可以有多个双向绑定:

vue 复制代码
<!-- components/DateRange.vue -->
<script setup lang="ts">
const startDate = defineModel<Date>('startDate')
const endDate = defineModel<Date>('endDate')
</script>

<template>
  <div class="date-range">
    <input type="date" v-model="startDate" />
    <span>至</span>
    <input type="date" v-model="endDate" />
  </div>
</template>

使用:

vue 复制代码
<script setup lang="ts">
const start = ref<Date>()
const end = ref<Date>()
</script>

<template>
  <DateRange v-model:start-date="start" v-model:end-date="end" />
</template>

九、透传属性

有时候组件只是一个包装器,需要把所有属性传给内部元素:

vue 复制代码
<!-- components/MyButton.vue -->
<script setup lang="ts">
// 不声明 props,属性会自动透传到根元素
</script>

<template>
  <button class="my-button">
    <slot />
  </button>
</template>

使用时,typedisabled 等属性会自动传给 <button>

vue 复制代码
<MyButton type="submit" disabled>提交</MyButton>

渲染结果:

html 复制代码
<button class="my-button" type="submit" disabled>提交</button>

禁用透传:

vue 复制代码
<script setup lang="ts">
defineOptions({
  inheritAttrs: false
})

const attrs = useAttrs()
</script>

<template>
  <div>
    <input v-bind="attrs" />
  </div>
</template>

十、组件引用

有时候需要调用子组件的方法:

vue 复制代码
<!-- components/Modal.vue -->
<script setup lang="ts">
const visible = ref(false)

const open = () => { visible.value = true }
const close = () => { visible.value = false }

// 暴露方法给父组件
defineExpose({ open, close })
</script>

<template>
  <Teleport to="body">
    <div v-if="visible" class="modal">
      <slot />
      <button @click="close">关闭</button>
    </div>
  </Teleport>
</template>

父组件:

vue 复制代码
<script setup lang="ts">
const modalRef = ref<{ open: () => void; close: () => void }>()

const showModal = () => {
  modalRef.value?.open()
}
</script>

<template>
  <button @click="showModal">打开弹窗</button>
  <Modal ref="modalRef">
    <h2>弹窗内容</h2>
  </Modal>
</template>

总结

组件通信技巧汇总:

方式 方向 场景
Props 父→子 传递数据
Emit 子→父 传递事件
v-model 双向 表单组件
插槽 父→子 内容分发
作用域插槽 子→父 子组件数据供父组件渲染
defineExpose 父→子 调用子组件方法

下一篇聊聊全局样式与 CSS 模块,让你的样式管理更规范。

相关文章

入门篇三:Nuxt4组件自动导入:写代码少敲一半字

入门篇二:Nuxt 4路由自动生成:告别手动配置路由的日子

延伸阅读

nuxt4完整系列,持续更新中。。,欢迎来逛逛


内容有帮助?点赞、收藏、关注三连!评论区等你 💪

相关推荐
kerli2 小时前
Compose 组件:LazyColumn 核心参数与 key/contentType 详解
android·前端
好运的阿财2 小时前
“锟斤拷”问题——程序中用powershell执行命令出现中文乱码的解决办法
linux·前端·人工智能·机器学习·架构·编辑器·vim
踩着两条虫2 小时前
VTJ.PRO AI + 低代码实战:接入高德地图
前端·vue.js·ai编程
绝世唐门三哥2 小时前
React性能优化:memo、useMemo和useCallback全解析
前端·react.js·memo
兔子零10242 小时前
Claude Code 都把宠物养进终端了,我做了一个真正能长期玩的中文宠物游戏
前端·游戏·游戏开发
xiaotao1312 小时前
Vite 与 Webpack 开发/打包时环境变量对比
前端·vue.js·webpack
摆烂工程师2 小时前
教你如何查询 Codex 最新额度是多少,以及 ChatGPT Pro、Plus、Business 最新额度变化
前端·后端·ai编程
捧月华如2 小时前
响应式设计原理与实践:适配多端设备的前端秘籍
前端·前端框架·json
笨笨狗吞噬者3 小时前
VSCode 插件推荐 Copy Filename Pro,快速复制文件、目录和路径的首选
前端·visual studio code