学习vue第十三天 Vue3组件深入指南:组件的艺术与科学

🧩 Vue3组件深入指南:组件的艺术与科学

🎯 本章学习地图

复制代码
Vue3组件深入
    ├── 🏗️ 组件拆分与嵌套(搭积木)
    ├── 🎨 组件样式特性(化妆术)
    ├── 📡 组件间通信(传话游戏)
    │   ├── Props(父→子)
    │   ├── Emit(子→父)
    │   ├── Provide/Inject(祖→孙)
    │   └── EventBus(兄弟间)
    └── 🎪 组件插槽(魔法口袋)
        ├── 默认插槽
        ├── 具名插槽
        └── 作用域插槽

🏗️ 第一部分:组件拆分与嵌套

🎭 什么是组件拆分?

想象你在搭乐高城堡🏰:

  • 不是一块巨大的积木 ❌
  • 而是很多小积木组合 ✅

🎪 实际案例:网页布局拆分

vue 复制代码
<!-- ❌ 不好的做法:所有代码混在一起 -->
<template>
  <div class="page">
    <!-- 头部 -->
    <header>...</header>
    <!-- 主体 -->
    <main>
      <!-- 轮播图 -->
      <div class="banner">...</div>
      <!-- 商品列表 -->
      <div class="products">...</div>
    </main>
    <!-- 底部 -->
    <footer>...</footer>
  </div>
</template>

<!-- ✅ 好的做法:组件化拆分 -->
<template>
  <div class="app">
    <Header />
    <Main />
    <Footer />
  </div>
</template>

<script>
import Header from './Header.vue'
import Main from './Main.vue'
import Footer from './Footer.vue'

export default {
  components: { Header, Main, Footer }
}
</script>

🎯 组件嵌套:俄罗斯套娃

vue 复制代码
<!-- App.vue:最外层套娃 -->
<template>
  <div class="app">
    <Header />
    <Main />      <!-- 里面还有套娃 -->
    <Footer />
  </div>
</template>

<!-- Main.vue:中间层套娃 -->
<template>
  <main>
    <MainBanner />        <!-- 更小的套娃 -->
    <MainProductList />   <!-- 更小的套娃 -->
  </main>
</template>

<!-- MainBanner.vue:最里层套娃 -->
<template>
  <div class="banner">
    我是轮播图组件
  </div>
</template>

💡 拆分原则

javascript 复制代码
const 组件拆分原则 = {
  // ✅ 应该拆分的情况
  "功能独立": "有独立的功能逻辑",
  "可复用": "会在多个地方使用",
  "代码量大": "超过200行代码",
  "职责单一": "只做一件事情",
  
  // ❌ 不应该拆分的情况
  "过度拆分": "就几行代码也拆成组件",
  "紧密耦合": "拆了反而更复杂",
  "无复用价值": "只用一次且逻辑简单"
}

🎨 第二部分:组件样式特性

🎭 样式作用域:各管各的地盘

1. Scoped样式(私人领地)
vue 复制代码
<!-- HelloWorld.vue -->
<template>
  <div class="text">Hello World组件的文本</div>
</template>

<style scoped>
/* 🔒 这个样式只在HelloWorld组件内生效 */
.text {
  color: red;
}
</style>
vue 复制代码
<!-- App.vue -->
<template>
  <div class="text">App组件的文本</div>  <!-- 不会变红 -->
  <HelloWorld />  <!-- 这个会变红 -->
</template>

<style scoped>
.text {
  color: blue;  /* App的text是蓝色 */
}
</style>

原理:Vue会给每个元素加上唯一的属性

html 复制代码
<!-- 编译后 -->
<div class="text" data-v-123abc>App组件的文本</div>
<div class="text" data-v-456def>Hello World组件的文本</div>

<style>
.text[data-v-123abc] { color: blue; }
.text[data-v-456def] { color: red; }
</style>
2. 深度选择器(穿墙术)
vue 复制代码
<template>
  <div class="app">
    <HelloWorld />
  </div>
</template>

<style scoped>
/* ❌ 无法影响子组件 */
.msg {
  color: red;
}

/* ✅ 使用深度选择器可以影响子组件 */
:deep(.msg) {
  color: red;
}

/* 或者使用 >>> 或 /deep/ (旧语法) */
>>> .msg { color: red; }
/deep/ .msg { color: red; }
</style>

比喻

  • scoped:每个房间有自己的钥匙🔑
  • :deep():万能钥匙,可以打开子房间的门🗝️
3. CSS Modules(模块化)
vue 复制代码
<template>
  <div :class="$style.text">CSS Modules文本</div>
</template>

<style module>
.text {
  color: green;
}
</style>
4. v-bind in CSS(动态样式)
vue 复制代码
<template>
  <div class="text">动态颜色文本</div>
  <button @click="changeColor">改变颜色</button>
</template>

<script>
export default {
  data() {
    return {
      color: 'red'
    }
  },
  methods: {
    changeColor() {
      this.color = this.color === 'red' ? 'blue' : 'red'
    }
  }
}
</script>

<style scoped>
.text {
  /* 🎨 CSS中使用JS变量 */
  color: v-bind(color);
}
</style>

📡 第三部分:组件间通信

🎭 通信方式全家福

javascript 复制代码
const 组件通信方式 = {
  "父→子": "Props(最常用)",
  "子→父": "Emit事件(最常用)",
  "祖→孙": "Provide/Inject(跨层级)",
  "兄弟间": "EventBus(事件总线)",
  "任意组件": "Vuex/Pinia(状态管理)"
}

📤 1. Props:父传子(快递员)

🎪 基本用法
vue 复制代码
<!-- 父组件:App.vue -->
<template>
  <ShowMessage 
    title="我是标题" 
    :content="content"
  />
</template>

<script>
import ShowMessage from './ShowMessage.vue'

export default {
  components: { ShowMessage },
  data() {
    return {
      content: "我是内容"
    }
  }
}
</script>
vue 复制代码
<!-- 子组件:ShowMessage.vue -->
<template>
  <div>
    <h2>{{ title }}</h2>
    <p>{{ content }}</p>
  </div>
</template>

<script>
export default {
  // 🎯 方式1:数组形式(简单)
  props: ['title', 'content']
  
  // 🎯 方式2:对象形式(推荐)
  props: {
    title: String,
    content: {
      type: String,
      required: true,
      default: '默认内容'
    }
  }
}
</script>
🎯 Props类型验证
javascript 复制代码
export default {
  props: {
    // 基础类型检查
    age: Number,
    name: String,
    isVIP: Boolean,
    
    // 多种类型
    price: [Number, String],
    
    // 必填项
    id: {
      type: Number,
      required: true
    },
    
    // 带默认值
    message: {
      type: String,
      default: '默认消息'
    },
    
    // 对象/数组默认值(必须用函数返回)
    user: {
      type: Object,
      default() {
        return { name: '游客' }
      }
    },
    
    // 自定义验证
    score: {
      type: Number,
      validator(value) {
        return value >= 0 && value <= 100
      }
    }
  }
}
🎪 Props传递技巧
vue 复制代码
<!-- 1. 传递静态值 -->
<MyComponent title="静态标题" />

<!-- 2. 传递动态值 -->
<MyComponent :title="dynamicTitle" />

<!-- 3. 传递对象属性 -->
<MyComponent :title="user.name" :age="user.age" />

<!-- 4. 批量传递对象属性(v-bind对象) -->
<MyComponent v-bind="user" />
<!-- 等同于 -->
<MyComponent :name="user.name" :age="user.age" :email="user.email" />
⚠️ Props注意事项
javascript 复制代码
// ❌ 不要在子组件中修改props
export default {
  props: ['count'],
  methods: {
    increment() {
      this.count++  // ❌ 错误!不能直接修改props
    }
  }
}

// ✅ 正确做法:使用本地data或computed
export default {
  props: ['count'],
  data() {
    return {
      localCount: this.count  // 复制到本地
    }
  },
  computed: {
    doubleCount() {
      return this.count * 2  // 基于props计算
    }
  }
}

📥 2. Emit:子传父(回信)

🎪 基本用法
vue 复制代码
<!-- 子组件:CounterOperation.vue -->
<template>
  <div>
    <button @click="increment">+1</button>
    <button @click="decrement">-1</button>
    <button @click="addTen">+10</button>
  </div>
</template>

<script>
export default {
  // 🎯 声明可以触发的事件
  emits: ['add', 'sub', 'addN'],
  
  methods: {
    increment() {
      // 🚀 触发add事件
      this.$emit('add')
    },
    decrement() {
      this.$emit('sub')
    },
    addTen() {
      // 🚀 触发事件并传递参数
      this.$emit('addN', 10)
    }
  }
}
</script>
vue 复制代码
<!-- 父组件:App.vue -->
<template>
  <div>
    <h4>当前计数: {{ counter }}</h4>
    <CounterOperation 
      @add="addOne"
      @sub="subOne"
      @addN="addNNum"
    />
  </div>
</template>

<script>
import CounterOperation from './CounterOperation.vue'

export default {
  components: { CounterOperation },
  data() {
    return {
      counter: 0
    }
  },
  methods: {
    addOne() {
      this.counter++
    },
    subOne() {
      this.counter--
    },
    addNNum(num) {
      this.counter += num
    }
  }
}
</script>
🎯 Emit验证
javascript 复制代码
export default {
  // 🎯 对象形式:可以验证事件参数
  emits: {
    // 无验证
    click: null,
    
    // 带验证
    submit(payload) {
      if (payload.email && payload.password) {
        return true
      } else {
        console.warn('Invalid submit event payload!')
        return false
      }
    }
  }
}

🌳 3. Provide/Inject:祖孙通信(家族遗产)

🎭 比喻:爷爷的遗产
复制代码
👴 爷爷(App)
  └─ 👨 爸爸(Home)
      └─ 👶 孙子(HomeContent)

爷爷想给孙子留遗产,不需要通过爸爸中转!

🎪 基本用法
vue 复制代码
<!-- 爷爷组件:App.vue -->
<template>
  <div class="app">
    <Home />
    <button @click="addFriend">添加朋友</button>
  </div>
</template>

<script>
import { computed } from 'vue'
import Home from './Home.vue'

export default {
  components: { Home },
  
  // 🎁 提供数据(provide)
  provide() {
    return {
      name: 'why',
      age: 18,
      friends: this.friends,  // 响应式数据
      friendLength: computed(() => this.friends.length)  // 计算属性
    }
  },
  
  data() {
    return {
      friends: ['jack', 'rose']
    }
  },
  
  methods: {
    addFriend() {
      this.friends.push('tony')
    }
  }
}
</script>
vue 复制代码
<!-- 孙子组件:HomeContent.vue -->
<template>
  <div>
    <h2>姓名: {{ name }}</h2>
    <h2>年龄: {{ age }}</h2>
    <h2>朋友: {{ friends }}</h2>
    <h2>朋友数量: {{ friendLength }}</h2>
  </div>
</template>

<script>
export default {
  // 🎁 接收数据(inject)
  inject: ['name', 'age', 'friends', 'friendLength'],
  
  created() {
    console.log(this.name, this.age)
  }
}
</script>
⚠️ 响应式注意事项
javascript 复制代码
// ❌ 普通值不是响应式的
provide() {
  return {
    count: this.count  // 不会响应式更新
  }
}

// ✅ 使用computed包装成响应式
provide() {
  return {
    count: computed(() => this.count)  // 响应式更新
  }
}

🚌 4. EventBus:事件总线(广播站)

🎭 比喻:校园广播站
复制代码
📻 广播站(EventBus)
  ├─ 🎤 发送者(About组件)
  └─ 📡 接收者(Home组件)
🎪 实现方式
javascript 复制代码
// utils/eventbus.js
import mitt from 'mitt'

// 创建事件总线
const emitter = mitt()

export default emitter
vue 复制代码
<!-- 发送者:About.vue -->
<template>
  <button @click="sendMessage">发送消息</button>
</template>

<script>
import emitter from './utils/eventbus'

export default {
  methods: {
    sendMessage() {
      // 📡 发送广播
      emitter.emit('message', {
        content: 'Hello from About!',
        time: Date.now()
      })
    }
  }
}
</script>
vue 复制代码
<!-- 接收者:Home.vue -->
<script>
import emitter from './utils/eventbus'

export default {
  created() {
    // 📻 监听广播
    emitter.on('message', (data) => {
      console.log('收到消息:', data.content)
    })
  },
  
  unmounted() {
    // 🔇 取消监听(重要!)
    emitter.off('message')
  }
}
</script>
⚠️ EventBus注意事项
javascript 复制代码
// ❌ 忘记取消监听会导致内存泄漏
created() {
  emitter.on('event', this.handler)
}
// 组件销毁了,但监听器还在!

// ✅ 记得在组件销毁时取消监听
unmounted() {
  emitter.off('event', this.handler)
}

🎪 第四部分:组件插槽

🎭 什么是插槽?

想象插槽是一个魔法口袋🎒:

  • 组件定义口袋的位置和大小
  • 使用者决定往口袋里放什么

🎯 1. 默认插槽(单口袋)

vue 复制代码
<!-- 子组件:MySlotCpn.vue -->
<template>
  <div class="slot-container">
    <h2>我是组件标题</h2>
    <!-- 🎒 这是一个魔法口袋 -->
    <slot>
      <!-- 默认内容:口袋空着时显示 -->
      <p>默认内容</p>
    </slot>
  </div>
</template>
vue 复制代码
<!-- 父组件使用 -->
<template>
  <!-- 1. 不放东西:显示默认内容 -->
  <MySlotCpn></MySlotCpn>
  
  <!-- 2. 放一个按钮 -->
  <MySlotCpn>
    <button>我是按钮</button>
  </MySlotCpn>
  
  <!-- 3. 放文本 -->
  <MySlotCpn>
    我是普通文本
  </MySlotCpn>
  
  <!-- 4. 放多个元素 -->
  <MySlotCpn>
    <span>我是span</span>
    <button>我是button</button>
    <strong>我是strong</strong>
  </MySlotCpn>
</template>

🎯 2. 具名插槽(多口袋)

🎭 比喻:导航栏的三个口袋
复制代码
┌─────────────────────────────┐
│ [左口袋] [中间口袋] [右口袋] │
└─────────────────────────────┘
vue 复制代码
<!-- 子组件:NavBar.vue -->
<template>
  <div class="nav-bar">
    <div class="left">
      <!-- 🎒 左边的口袋 -->
      <slot name="left"></slot>
    </div>
    <div class="center">
      <!-- 🎒 中间的口袋 -->
      <slot name="center"></slot>
    </div>
    <div class="right">
      <!-- 🎒 右边的口袋 -->
      <slot name="right"></slot>
    </div>
  </div>
</template>
vue 复制代码
<!-- 父组件使用 -->
<template>
  <NavBar>
    <!-- 🎯 往左边口袋放按钮 -->
    <template #left>
      <button>返回</button>
    </template>
    
    <!-- 🎯 往中间口袋放标题 -->
    <template #center>
      <h4>我是标题</h4>
    </template>
    
    <!-- 🎯 往右边口袋放图标 -->
    <template #right>
      <i class="icon">⚙️</i>
    </template>
  </NavBar>
</template>
🎯 具名插槽语法
vue 复制代码
<!-- 完整写法 -->
<template v-slot:left>
  <button>返回</button>
</template>

<!-- 简写(推荐) -->
<template #left>
  <button>返回</button>
</template>

<!-- 动态插槽名 -->
<template #[slotName]>
  <button>动态内容</button>
</template>

🎯 3. 作用域插槽(智能口袋)

🎭 比喻:会说话的口袋

普通口袋:只能放东西

智能口袋:还能告诉你里面有什么

vue 复制代码
<!-- 子组件:ShowNames.vue -->
<template>
  <div>
    <template v-for="(item, index) in names" :key="index">
      <!-- 🎒 智能口袋:把数据传出去 -->
      <slot :item="item" :index="index">
        <!-- 默认显示方式 -->
        <span>{{ item }}</span>
      </slot>
    </template>
  </div>
</template>

<script>
export default {
  props: {
    names: {
      type: Array,
      default: () => []
    }
  }
}
</script>
vue 复制代码
<!-- 父组件使用 -->
<template>
  <ShowNames :names="names">
    <!-- 🎯 接收口袋传出的数据 -->
    <template v-slot="slotProps">
      <button>{{ slotProps.item }} - {{ slotProps.index }}</button>
    </template>
  </ShowNames>
  
  <!-- 🎯 解构写法(推荐) -->
  <ShowNames :names="names">
    <template v-slot="{ item, index }">
      <span>{{ item }} - {{ index }}</span>
    </template>
  </ShowNames>
  
  <!-- 🎯 简写 -->
  <ShowNames :names="names" v-slot="{ item, index }">
    <span>{{ item }} - {{ index }}</span>
  </ShowNames>
</template>

<script>
export default {
  data() {
    return {
      names: ['why', 'kobe', 'james', 'curry']
    }
  }
}
</script>
🎪 作用域插槽应用场景
vue 复制代码
<!-- 场景1:列表渲染自定义 -->
<UserList :users="users">
  <template #default="{ user }">
    <div class="user-card">
      <img :src="user.avatar" />
      <h3>{{ user.name }}</h3>
    </div>
  </template>
</UserList>

<!-- 场景2:表格列自定义 -->
<DataTable :data="tableData">
  <template #column-name="{ row }">
    <strong>{{ row.name }}</strong>
  </template>
  <template #column-action="{ row }">
    <button @click="edit(row)">编辑</button>
    <button @click="delete(row)">删除</button>
  </template>
</DataTable>

⚠️ 注意事项和常见坑

🕳️ 坑1:Props单向数据流

javascript 复制代码
// ❌ 错误:直接修改props
export default {
  props: ['count'],
  methods: {
    increment() {
      this.count++  // 报错!
    }
  }
}

// ✅ 正确:通过emit通知父组件修改
export default {
  props: ['count'],
  emits: ['update:count'],
  methods: {
    increment() {
      this.$emit('update:count', this.count + 1)
    }
  }
}

🕳️ 坑2:忘记声明emits

javascript 复制代码
// ❌ 不声明emits(虽然能用,但不规范)
export default {
  methods: {
    handleClick() {
      this.$emit('click')  // 没有声明
    }
  }
}

// ✅ 声明emits(推荐)
export default {
  emits: ['click'],
  methods: {
    handleClick() {
      this.$emit('click')
    }
  }
}

🕳️ 坑3:插槽作用域混淆

vue 复制代码
<template>
  <ChildComponent>
    <!-- ❌ 这里访问的是父组件的数据 -->
    <div>{{ parentData }}</div>
    
    <!-- ✅ 要访问子组件数据需要作用域插槽 -->
    <template v-slot="{ childData }">
      <div>{{ childData }}</div>
    </template>
  </ChildComponent>
</template>

🕳️ 坑4:Provide/Inject响应式丢失

javascript 复制代码
// ❌ 直接provide值不是响应式的
provide() {
  return {
    count: this.count  // 不会更新
  }
}

// ✅ 使用computed保持响应式
provide() {
  return {
    count: computed(() => this.count)  // 会更新
  }
}

🎯 最佳实践

✅ 1. 组件拆分策略

javascript 复制代码
const 拆分策略 = {
  // 按功能拆分
  "Header": "头部导航",
  "Sidebar": "侧边栏",
  "Content": "主内容区",
  
  // 按复用性拆分
  "Button": "通用按钮",
  "Card": "通用卡片",
  "Modal": "通用弹窗",
  
  // 按业务拆分
  "UserProfile": "用户资料",
  "ProductList": "商品列表",
  "ShoppingCart": "购物车"
}

✅ 2. 通信方式选择

javascript 复制代码
const 通信选择 = {
  "父子通信": "Props + Emit(首选)",
  "跨层级通信": "Provide/Inject",
  "兄弟通信": "EventBus或状态管理",
  "复杂状态": "Vuex/Pinia(状态管理)"
}

✅ 3. 插槽使用建议

javascript 复制代码
const 插槽建议 = {
  "简单内容替换": "默认插槽",
  "多个位置定制": "具名插槽",
  "需要子组件数据": "作用域插槽",
  "提供默认内容": "插槽默认值"
}

💡 记忆口诀

组件拆分像搭积木,职责单一好维护
Props父传子,Emit子传父
Provide祖传孙,EventBus兄弟通
插槽是口袋,具名分左右
作用域插槽,数据能传出


🎪 总结

Vue3组件深入就像:

  • 🏗️ 组件拆分:搭乐高积木,模块化开发
  • 🎨 样式特性:各管各的地盘,互不干扰
  • 📡 组件通信:传话游戏,各有各的方式
  • 🎪 组件插槽:魔法口袋,灵活定制内容

🎯 核心要点

  1. 组件拆分:合理拆分,避免过度
  2. 样式隔离:使用scoped,必要时用:deep()
  3. Props验证:类型检查,提高健壮性
  4. Emit声明:明确事件,规范开发
  5. 插槽灵活:根据需求选择合适的插槽类型

掌握这些技能,你就能构建出结构清晰、易于维护的Vue3应用!🎉


📚 建议配合实际项目练习使用。

相关推荐
@PHARAOH1 小时前
WHAT - Vercel react-best-practices 系列(二)
前端·javascript·react.js
qq_406176141 小时前
深入理解 JavaScript 闭包:从原理到实战避坑
开发语言·前端·javascript
float_六七2 小时前
JavaScript变量声明:var的奥秘
开发语言·前端·javascript
zhengxianyi5152 小时前
ruoyi-vue-pro本地环境搭建(超级详细,带异常处理)
前端·vue.js·前后端分离·ruoyi-vue-pro
心枢AI研习社2 小时前
python学习笔记8--破茧与连接:Python HTTP 全球协作实战复盘
笔记·python·学习
小黄人软件2 小时前
蓝色清爽discuz门户论坛模板 ENET新锐版 安装
学习
老邓计算机毕设2 小时前
SSM学期分析与学习行为分析系统c8322(程序+源码+数据库+调试部署+开发环境)带论文文档1万字以上,文末可获取,系统界面在最后面
数据库·学习·ssm 框架·学期分析·学习行为分析
不解风水2 小时前
【自动控制原理】学习笔记
笔记·学习·自动控制原理
研☆香2 小时前
JavaScript 特点介绍
开发语言·javascript·ecmascript