Lecture08-组件:定义、插槽、props、事件

文章目录

    [toc]

    单词词性音标含义
    componentn./kəmˈpəʊnənt/组件,一块可复用的“积木”,一个组件 = 一个 .vue 文件
    slotn./slɒt/插槽,子组件里留的“空位”,父组件往里塞内容
    propsn./prɒps/属性(参数),父组件传给子组件的数据,只读
    emitv./iˈmɪt/触发(事件),子组件用 emit 把数据上报给父组件
    reusableadj./riˈjuːzəbl/可复用的,组件化的核心价值
    scopedadj./ˈskəʊpt/作用域受限的,样式只对当前组件生效
    communicationn./kəˌmjuːnɪˈkeɪʃn/通信,组件之间传递数据的各种方式
    defineProps-/dɪˈfaɪn prɒps/编译宏,声明这个组件接收哪些 props

    Lecture08-组件:定义、插槽、props、事件

    1、本节讲什么

    学会"把页面拆成积木":定义组件、用组件拼成页面、用插槽定制内容、用 props 把数据传进子组件、用 emit 事件把数据从子组件传回父组件。这是 Vue 组件复用的全部地基,也是后面做后台管理系统的核心。

    说明:本节合并了"组件基础 + 组件通信",通过 4 个实验由浅入深把组件一次讲透。

    2、概念速览

    什么是组件:把一块页面(结构 <template> + 样式 <style> + 行为 <script>)打包成一个可复用的"积木"。一个组件 = 一个 .vue 文件。

    image-20261002160003186

    组件的作用:① 复用——一处写好、到处用;② 分工——页面拆成小积木,各管各的、好维护;③ 独立——scoped 让样式只对自己生效。 image-20261002161434982

    Vue组件的三个组成部分:

    • 【结构】template 标签:编写 HTML 代码,也可以引入其它组件
    • 【行为】script 标签:编写 JavaScript、TypeScript 代码
    • 【表现】style 标签:编写 CSS 代码

    Vue组件文件的扩展名:*.vue

    组件的三种数据关系(本节学前两种,Pinia 在第 18 节):

    • 纵向:父 → 子用 props(数据下传);子 → 父用 emit 事件(消息上报)。
    • 横向:兄弟组件之间没有"直连线",要么借父组件中转,要么上 Pinia(详见本节常见疑问 + 配图)。
    • 全局共享:Pinia。

    正确 vs 误区:组件不是"一个文件"这么简单,核心是"独立、可复用、只干一件事";一个页面 = 多个组件组装,而不是一个大文件堆到底。

    3、先看效果

    做一个最简单的"页面 = 组件拼出来"的演示——两个小组件拼成一个带顶部栏和按钮的页面(不涉及任何数据传递,先看组件怎么组合):

    src/components/Header.vue(子组件:顶部栏)

    <template>
      <header class="top">后台管理系统</header>
    </template>
    
    <style scoped>
        .top { height: 50px; background: #42b883; color: #fff; line-height: 50px; text-align: center; }
    </style>
    

    src/components/MyButton.vue(子组件:一个按钮)

    <template>
      <button class="btn">默认按钮</button>
    </template>
    
    <style scoped>
    .btn { padding: 8px 16px; cursor: pointer; }   /* 父组件管"布局位置",子组件管"自己长什么样" */
    </style>
    

    App.vue(父组件:把两个子组件组装成页面)

    <script setup lang="ts">
    import Header from './components/Header.vue'
    import MyButton from './components/MyButton.vue'
    </script>
    
    <template>
      <Header />
      <div class="layout"><MyButton /></div>
    </template>
    
    <style scoped>
    .layout { padding: 20px; }   /* 父组件管"布局位置",子组件管"自己长什么样" */
    </style>
    

    运行后:顶部一条栏 + 中间一个按钮,页面就是由多个小组件拼起来的。

    4、照着做:4 个实验

    实验 1 · 创建并使用组件(不传数据,观察组合)

    步骤 1:创建子组件。新建 src/components/Header.vue 和 MyButton.vue(如上)。

    步骤 2:在父组件引入子组件。在 App.vue 里 import,并在模板里用 <Header />、<MyButton /> 摆放。

    步骤 3:编写必要的样式,控制各组件位置。

    • 子组件用 <style scoped> 管自己内部的样式(高度、颜色)。
    • 父组件用布局(如 flex、padding)管组件在页面里的位置。

    体会:子组件"管自己长什么样",父组件"管怎么摆"——职责分离。

    实验 2 · 父组件通过插槽定制内容

    步骤 1:在子组件中编写 slot 标签(留一个"空位"):

    <!-- src/components/MyButton.vue -->
    <template>
      <button class="btn"><slot /></button>
    </template>
    

    <slot /> 就是插槽——一个空位,等外面往里塞内容。

    步骤 2:在父组件往插槽位置传入内容:

    <MyButton>登录</MyButton>
    <MyButton>注册</MyButton>
    

    <MyButton> 和 </MyButton> 之间写的内容,会显示在子组件的 <slot /> 位置。同一个按钮组件,通过插槽就能显示不同文字——组件留白、外面定制。

    实验 3 · 父组件通过 props 把数据传入子组件

    步骤 1:在子组件创建 props 对象(先写 interface,好理解;再用简化写法):

    <!-- src/components/MyCard.vue -->
    <script setup lang="ts">
    // 写法一:interface 声明"这个组件收哪些参数",清晰好懂
    interface Props {
      name: string
      age: number
    }
    const props = defineProps<Props>()
    
    // 写法二:简化写法(内联),和写法一完全等价
    // const props = defineProps<{ name: string; age: number }>()
    </script>
    

    步骤 2:把 props 的各个属性渲染到 HTML:

    <template>
      <p>{{ props.name }} · {{ props.age }} 岁</p>
    </template>
    

    步骤 3:在父组件给各个 props 属性赋值(注意加不加冒号):

    <MyCard name="小红" :age="22" />
    

    加不加冒号的区别:name="小红" 不带冒号,传的是字符串字面量;:age="22" 带冒号(v-bind 简写),传的是数字/变量/表达式。要传数字、布尔、数组、变量,必须用 :xxx。

    实验 4 · 子组件通过 emit 事件把数据向上传给父组件

    步骤 1:在子组件定义 emit 事件:

    <!-- src/components/MyCounter.vue -->
    <script setup lang="ts">
    // 声明:这个组件能触发 add 事件,带一个 number 参数
    const emit = defineEmits<{ add: [num: number] }>()
    </script>
    <template>
      <button @click="emit('add', 1)">点我 +1</button>
    </template>
    

    image-20261003163505329

    步骤 2:在子组件调用 emit 并传入数据——上面 @click="emit('add', 1)" 就是触发 add 事件、把 1 作为数据传出去。

    步骤 3:在父组件监听子组件的 emit 事件并接收数据:

    <script setup lang="ts">
    import MyCounter from './components/MyCounter.vue'
    import { ref } from 'vue'
    const total = ref(0)
    function onAdd(n: number) { total.value += n }
    </script>
    <template>
      <MyCounter @add="onAdd" />
      <p>点击数:{{ total }}</p>   <!-- 步骤4:把收到的数据渲染到 HTML -->
    </template>
    

    步骤 4:父组件把从事件接收到的数据渲染到 HTML——上面 <p>点击数:{{ total }}</p> 就是。

    方向记忆:props 是父发(: 传下去)子收;emit 是子发(emit('add', n))父收(@add)。 值永远从子组件那边 emit 带出来。

    5、常见疑问

    • Q:子组件之间如何横向传递数据? A:Vue 里兄弟组件没有"直连线",只有三种方式(见下图):

      • ① 借父组件中转(最常用):A 发事件给父 W,父更新自己的数据,再通过 props 传给 B——本质是"两条纵向"拼出来的。
      • ② Pinia 全局共享(第 18 节):A、B 都去读写同一个全局仓库,绕开父子关系,跨组件、数据多时最省事。
      • ③ 事件总线(不推荐):让兄弟"直连",但数据流隐蔽、难追踪,是屎山诱因。 component-communication
      • ④另外还有一种方式是在路由跳转过程中携带参数,但通常认为这种情况属于『页面』之间传参
      • ⑤总结:每一种数据传递的技术,都对应一个特定的场景;特定场景使用恰当的技术来传递数据,有利于优化项目的代码结构
    • Q:props 和 Java 的方法参数像吗? A:很像——父组件"传参",子组件声明"接收哪些参数"。好处是组件可复用、可参数化。

    • Q:name="小红" 和 :age="22" 有什么区别? A:不带冒号传字符串字面量;带冒号(v-bind 简写)传数字/变量/表达式。传数字、布尔、数组必须用 :xxx。

    • Q:子组件能改 props 吗? A:不能。props 是父传子、只读。子组件要改,就 emit 事件让父组件改——这叫"单向数据流",保证数据流向清晰、好排查。 image-20261008113222662

    • Q:为什么 props 不能被 Pinia 完全取代? A:props 是"组件复用地基"——让组件可参数化(同一个卡片,传不同数据显示不同内容),这是组件复用的本质。Pinia 是"全局共享仓库",管的是"多个不相干组件共用的数据"。局部传参用 props,全局共享才用 Pinia,全塞 Pinia 反而更乱(详见第 18 节对比)。

    • Q:slot 到底是什么? A:给组件留的一个"空位"。外面往空位塞什么,就显示什么。让组件能"留白"、更通用,不用每次写死内容。

    • Q:如果 Vue 插件提供的组件骨架代码片段快捷方式无效或不稳定,怎么解决? A:Ctrl + Shift + P image-20261008102347206 image-20261008102428134 使用下面代码片段覆盖原内容:

      {
        "Vue3 TS Component": {
          "prefix": "vue3",
          "body": [
            "<script setup lang=\"ts\">",
            "",
            "</script>",
            "",
            "<template>",
            "  <div></div>",
            "</template>",
            "",
            "<style scoped>",
            "",
            "</style>"
          ],
          "description": "Vue3 setup ts component"
        }
      }
      

    6、规范要点

    • 组件职责单一:一个组件只做一件事,复杂了就拆;页面 = 组件组装。
    • 组件命名大驼峰(MyCard.vue),文件名和组件名一致,放 src/components/。
    • props 只读:子组件不改 props,靠 emit 通知父组件——这是防屎山的核心之一。
    • 组件对外接口明确:defineProps 负责接收数据、defineEmits 负责发送数据,不能乱。
    • 传复杂数据(对象/数组)用 :xxx,并配合 interface 标注类型。
    • 横向传数据优先借父组件中转或 Pinia,别用事件总线。
    • 通用小零件(按钮、卡片、表格)先想能不能用现成的(Element Plus,第 11 节),别急着自己造。
      • ElementPlus 提供具体控件
      • 组件根据业务的需求创建页面布局单元

    7、本节验证

    • 会拆组件:把页面拆成若干 .vue 积木再组装 ✅
    • 会用 <slot /> 让父组件定制子组件的内容 ✅
    • 会用 defineProps 接收数据,并分清 name="小红" 与 :age="22" ✅
    • 会用 defineEmits + emit('add', n) 把数据传回父组件 ✅
    • 知道 props 只读,兄弟组件横向传数据借父组件或 Pinia ✅
    end