下载工作台
前端框架精通

Vue 3 组件化与单文件组件

试读上半部分 · 解锁后可读全文

第 2 章 · Vue 3 组件化与单文件组件

本章目标:将 ch01 的内联模板拆分为可复用单文件组件(SFC);掌握 props / emits / slots、父子与跨层通信、scoped 样式CSS 变量;产出 CourseCard.vueCourseList.vue 完整实现。

学时建议:3~4 小时

前置:完成本模块 ch01;理解 ES 模块 import/export。


2.1 为什么组件化

问题(ch01 单文件)组件化收益
App.vue 超过 200 行难维护按 UI 边界拆分,职责单一
卡片 markup 重复CourseCard 一处修改全局生效
样式类名冲突scoped 隔离组件 CSS
难以单测纯 props 输入、emit 输出易测
行业案例 · 某在线教育 SaaS:课程卡片在首页、搜索结果、推荐侧栏三处复用。组件化后设计令牌(颜色、圆角)通过 CSS 变量统一下发,品牌换肤从改 40 个文件变为改 tokens.css 一个文件,QA 回归范围缩小 60%。

2.2 单文件组件(SFC)结构

<!-- CourseCard.vue -->
<script setup>
// 逻辑
</script>

<template>
  <!-- 结构 -->
</template>

<style scoped>
/* 样式,仅作用于本组件 */
</style>
编译结果
<script setup>组件 setup() 函数
<template>渲染函数
<style scoped>data-v-xxxxx 属性选择器

2.3 Props:父 → 子

父组件通过 props 向子组件传递数据;子组件只读,不可直接修改 props。

<!-- CourseCard.vue -->
<script setup>
const props = defineProps({
  course: {
    type: Object,
    required: true,
  },
  favorited: {
    type: Boolean,
    default: false,
  },
})

// 基于 props 的派生 — 用 computed,勿改 props
import { computed } from 'vue'
const levelLabel = computed(() => {
  const map = { beginner: '入门', intermediate: '进阶', advanced: '高级' }
  return map[props.course.level] ?? props.course.level
})
</script>

<template>
  <article class="course-card">
    <img
      v-if="course.cover"
      :src="course.cover"
      :alt="course.title"
      class="cover"
      loading="lazy"
    />
    <h3 class="title">{{ course.title }}</h3>
    <p class="desc">{{ course.desc }}</p>
    <div class="tags">
      <span v-for="tag in course.tags" :key="tag" class="tag">{{ tag }}</span>
    </div>
    <footer class="footer">
      <span>{{ course.hours }} 学时</span>
      <span>{{ levelLabel }}</span>
      <button
        type="button"
        class="fav-btn"
        :aria-pressed="favorited"
        @click="$emit('toggle-favorite', course.id)"
      >
        {{ favorited ? '已收藏' : '收藏' }}
      </button>
    </footer>
  </article>
</template>

Props 命名:脚本中用 camelCasefavorited),模板中可用 kebab-casefavorited:is-favorited)。


2.4 Emits:子 → 父

子组件通过 emit 通知父组件处理业务(改状态、调 API)。

<script setup>
const emit = defineEmits({
  'toggle-favorite': (id) => typeof id === 'string',
  'open-detail': (id) => typeof id === 'string',
})
</script>

父组件监听:

<CourseCard
  :course="c"
  :favorited="favoriteIds.has(c.id)"
  @toggle-favorite="onToggleFavorite"
  @open-detail="goDetail"
/>
模式说明
Props down数据、配置自父传子
Events up用户操作由子 emit,父改状态
禁止子改 props需改时父更新源数据

2.5 Slots:内容分发

当子组件需要「外壳固定、内部可定制」时使用插槽。

<!-- SearchBar.vue -->
<template>
  <div class="search-bar">
    <slot name="prefix" />
    <input
      :value="modelValue"
      type="search"
      :placeholder="placeholder"
      @input="$emit('update:modelValue', $event.target.value)"
    />
    <slot name="suffix">
      <span class="hint">Enter 搜索</span>
    </slot>
  </div>
</template>

<script setup>
defineProps({
  modelValue: { type: String, default: '' },
  placeholder: { type: String, default: '搜索…' },
})
defineEmits(['update:modelValue'])
</script>

父组件:

<SearchBar v-model="keyword">
  <template #prefix>
    <span aria-hidden="true">🔍</span>
  </template>
  <template #suffix>
    <button type="button" @click="keyword = ''">清空</button>
  </template>
</SearchBar>
插槽类型语法场景
默认插槽<slot />单块自定义内容
具名插槽<slot name="footer" />多区域布局
作用域插槽<slot :item="row" />父定制子列表每一项

以下内容需解锁后阅读

试读已结束。解锁本章 ¥5.00,或开通年度会员畅读全部教程。
年度会员 ¥199.00/年; 小紫 AI 工作台有效会员 ¥99.00/年

正文仅在服务端鉴权后下发,未付费无法获取下半部分内容。