跳到主要内容

第 6 节:组件基础

📚 本节目标

  • 理解什么是组件,为什么要用组件
  • 学会定义和使用全局组件
  • 掌握组件的嵌套使用
  • 学会使用 props 在父子组件间传递数据

一、什么是组件?

生活中的例子

想象你在搭积木:

  • 每块积木是一个组件
  • 把不同积木组合起来,就能搭出复杂的作品

代码中的组件

大白话:组件就是可以重复使用的代码块。

好处

  • 复用:写一次,用多次(比如按钮、卡片)
  • 维护:修改一处,全部更新
  • 清晰:把复杂页面拆成小块,更好理解

类比

  • 不用组件:每次都要从头写,就像每次做饭都要从种菜开始
  • 使用组件:直接用现成的,就像去超市买食材

二、创建第一个组件

全局组件的定义和使用

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>第一个组件</title>
<style>
.hello-card {
padding: 20px;
margin: 10px;
border: 2px solid #42b983;
border-radius: 10px;
background: #f0f9f4;
display: inline-block;
}
</style>
</head>
<body>
<div id="app">
<h2>组件演示</h2>

<!-- 使用组件:就像使用 HTML 标签一样 -->
<hello-card></hello-card>
<hello-card></hello-card>
<hello-card></hello-card>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

// 创建 Vue 应用
const app = createApp({});

// 定义全局组件
app.component('hello-card', {
// template:组件的 HTML 模板
template: `
<div class="hello-card">
<h3>👋 你好!</h3>
<p>我是一个可复用的组件</p>
</div>
`
});

// 挂载应用
app.mount('#app');
</script>
</body>
</html>

预期效果

  • 页面上显示 3 个相同的卡片
  • 只定义了一次组件,但用了 3 次

关键点解释

  1. app.component('组件名', { ... }):定义组件
  2. 组件名:建议用 kebab-case(小写+短横线),如 hello-card
  3. template:组件的 HTML 结构,用反引号包裹
  4. 使用组件:像 HTML 标签一样 <hello-card></hello-card>

三、组件也有自己的数据

组件内的 data

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>组件的数据</title>
<style>
.counter {
padding: 20px;
margin: 10px;
border: 2px solid #42b983;
border-radius: 10px;
display: inline-block;
text-align: center;
}
.counter button {
padding: 10px 20px;
background: #42b983;
color: white;
border: none;
border-radius: 5px;
cursor: pointer;
margin: 5px;
}
.count {
font-size: 32px;
font-weight: bold;
color: #42b983;
margin: 10px 0;
}
</style>
</head>
<body>
<div id="app">
<h2>独立的计数器组件</h2>

<!-- 每个组件都有自己独立的数据 -->
<counter-component></counter-component>
<counter-component></counter-component>
<counter-component></counter-component>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({});

app.component('counter-component', {
// 组件的 data 必须是函数
data() {
return {
count: 0
}
},
// 组件的方法
methods: {
increment() {
this.count++;
},
decrement() {
this.count--;
}
},
// 组件的模板
template: `
<div class="counter">
<h3>计数器</h3>
<div class="count">{{ count }}</div>
<button @click="increment">+1</button>
<button @click="decrement">-1</button>
</div>
`
});

app.mount('#app');
</script>
</body>
</html>

预期效果

  • 页面上有 3 个计数器
  • 点击任意一个计数器的按钮,只有它自己的数字会变化
  • 每个组件的数据是独立的

重要规则

  • 组件的 data 必须是函数(返回一个对象)
  • 原因:每个组件实例都需要有自己独立的数据副本

四、Props:父组件向子组件传递数据

什么是 Props?

大白话:Props 就像函数的参数,父组件可以给子组件传递数据。

类比

  • 父组件:工厂老板
  • 子组件:工人
  • Props:老板给工人的原材料

基本用法

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Props 基础</title>
<style>
.student-card {
padding: 20px;
margin: 10px;
border: 2px solid #42b983;
border-radius: 10px;
background: #f9f9f9;
display: inline-block;
width: 200px;
}
.student-card h3 {
margin-top: 0;
color: #42b983;
}
</style>
</head>
<body>
<div id="app">
<h2>学生卡片</h2>

<!-- 通过属性传递数据给子组件 -->
<student-card
name="小明"
:age="18"
major="计算机科学">
</student-card>

<student-card
name="小红"
:age="19"
major="软件工程">
</student-card>

<student-card
name="小刚"
:age="20"
major="人工智能">
</student-card>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({});

app.component('student-card', {
// props:声明这个组件接收哪些数据
props: ['name', 'age', 'major'],

// 在 template 中可以直接使用 props
template: `
<div class="student-card">
<h3>{{ name }}</h3>
<p>年龄:{{ age }} 岁</p>
<p>专业:{{ major }}</p>
</div>
`
});

app.mount('#app');
</script>
</body>
</html>

预期效果

  • 显示 3 个学生卡片
  • 每个卡片显示不同的姓名、年龄、专业

关键点

  1. 定义 propsprops: ['name', 'age', 'major']
  2. 传递数据
    • 字符串直接写:name="小明"
    • 数字/变量用 v-bind::age="18"
  3. 使用 props:在 template 中像 data 一样使用

Props 验证(推荐写法)

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>Props 验证</title>
<style>
.product-card {
padding: 20px;
margin: 10px;
border: 2px solid #ddd;
border-radius: 10px;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
width: 250px;
}
.product-card img {
width: 100%;
border-radius: 5px;
}
.price {
font-size: 24px;
color: #e74c3c;
font-weight: bold;
}
.stock {
color: #27ae60;
font-size: 14px;
}
.out-of-stock {
color: #e74c3c;
}
</style>
</head>
<body>
<div id="app">
<h2>商品卡片</h2>

<product-card
title="Vue 3 入门教程"
:price="99.9"
:stock="50"
image="https://via.placeholder.com/250x150?text=Vue+3">
</product-card>

<product-card
title="JavaScript 高级程序设计"
:price="129"
:stock="0"
image="https://via.placeholder.com/250x150?text=JavaScript">
</product-card>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({});

app.component('product-card', {
// Props 验证(更规范的写法)
props: {
title: {
type: String, // 类型:字符串
required: true // 必须传递
},
price: {
type: Number, // 类型:数字
required: true
},
stock: {
type: Number,
default: 0 // 默认值
},
image: {
type: String,
default: 'https://via.placeholder.com/250x150'
}
},

template: `
<div class="product-card">
<img :src="image" :alt="title">
<h3>{{ title }}</h3>
<div class="price">¥{{ price }}</div>
<p :class="stock > 0 ? 'stock' : 'out-of-stock'">
{{ stock > 0 ? '库存:' + stock : '已售罄' }}
</p>
</div>
`
});

app.mount('#app');
</script>
</body>
</html>

Props 验证的好处

  • 限制数据类型(String、Number、Boolean 等)
  • 设置必填项(required: true
  • 设置默认值(default: ...
  • 防止传错数据

五、父子组件通信:完整示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>父子组件通信</title>
<style>
.app-container {
max-width: 800px;
margin: 50px auto;
padding: 20px;
}
.control-panel {
background: #f0f0f0;
padding: 20px;
border-radius: 10px;
margin-bottom: 20px;
}
.control-panel button {
padding: 10px 20px;
margin: 5px;
cursor: pointer;
}
.message-box {
padding: 20px;
border: 2px solid #42b983;
border-radius: 10px;
background: #f0f9f4;
margin: 10px 0;
}
.message-box h3 {
margin-top: 0;
}
</style>
</head>
<body>
<div id="app">
<div class="app-container">
<h1>父子组件通信演示</h1>

<!-- 控制面板(父组件) -->
<div class="control-panel">
<h3>控制面板(父组件)</h3>
<button @click="changeMessage('你好!')">发送:你好!</button>
<button @click="changeMessage('Vue 很简单')">发送:Vue 很简单</button>
<button @click="changeMessage('继续加油!')">发送:继续加油!</button>
<p>当前消息:{{ message }}</p>
</div>

<!-- 消息展示组件(子组件) -->
<message-display :content="message"></message-display>
<message-display :content="message"></message-display>
</div>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({
data() {
return {
message: '欢迎学习 Vue!'
}
},
methods: {
changeMessage(newMessage) {
this.message = newMessage;
}
}
});

// 子组件:消息展示
app.component('message-display', {
props: {
content: {
type: String,
required: true
}
},
template: `
<div class="message-box">
<h3>📢 收到的消息(子组件)</h3>
<p>{{ content }}</p>
</div>
`
});

app.mount('#app');
</script>
</body>
</html>

预期效果

  • 点击父组件的按钮
  • 两个子组件同时显示新的消息
  • 数据流向:父组件 → 子组件(通过 props)

六、组件使用总结

组件定义的步骤

// 1. 创建应用
const app = createApp({ ... });

// 2. 定义组件
app.component('组件名', {
props: [...], // 接收的数据
data() { ... }, // 组件自己的数据
methods: { ... }, // 组件的方法
template: `...` // 组件的模板
});

// 3. 挂载应用
app.mount('#app');

数据流向规则

关系传递方式数据流向
父 → 子Props单向:父组件传给子组件
子 → 父事件(下节课讲)子组件触发,父组件监听
兄弟组件状态管理(进阶)通过共同的父组件或 Pinia

重要原则

  • ✅ 子组件可以读取 props
  • ❌ 子组件不能直接修改 props(单向数据流)

七、本节重点回顾

组件 = 可复用的代码块
全局组件app.component('组件名', { ... })
组件的 data 必须是函数,保证每个实例数据独立
Props:父组件向子组件传递数据
Props 验证:指定类型、必填、默认值
单向数据流:数据从父组件流向子组件


八、练习题

练习 1:创建一个按钮组件

要求:

  1. 组件名:custom-button
  2. 接收一个 prop:text(按钮文字)
  3. 接收一个 prop:color(按钮颜色,默认值 '#42b983'
  4. 点击按钮时弹出提示:你点击了:XXX
点击查看答案
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>自定义按钮组件</title>
</head>
<body>
<div id="app">
<custom-button text="确定"></custom-button>
<custom-button text="取消" color="#e74c3c"></custom-button>
<custom-button text="提交" color="#3498db"></custom-button>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({});

app.component('custom-button', {
props: {
text: {
type: String,
required: true
},
color: {
type: String,
default: '#42b983'
}
},
methods: {
handleClick() {
alert('你点击了:' + this.text);
}
},
template: `
<button
@click="handleClick"
:style="{
background: color,
color: 'white',
padding: '10px 20px',
border: 'none',
borderRadius: '5px',
cursor: 'pointer',
margin: '5px'
}">
{{ text }}
</button>
`
});

app.mount('#app');
</script>
</body>
</html>

练习 2:用户信息卡片列表

要求:

  1. 创建一个 user-card 组件
  2. 接收 props:name(姓名)、email(邮箱)、avatar(头像地址)
  3. 在父组件中用 v-for 循环显示多个用户卡片

数据参考:

users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com', avatar: 'https://i.pravatar.cc/150?img=1' },
{ id: 2, name: '李四', email: 'lisi@example.com', avatar: 'https://i.pravatar.cc/150?img=2' },
{ id: 3, name: '王五', email: 'wangwu@example.com', avatar: 'https://i.pravatar.cc/150?img=3' }
]
点击查看答案
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<title>用户卡片列表</title>
<style>
.user-card {
display: inline-block;
width: 200px;
padding: 20px;
margin: 10px;
border: 2px solid #ddd;
border-radius: 10px;
text-align: center;
background: white;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
}
.user-card img {
width: 100px;
height: 100px;
border-radius: 50%;
}
.user-card h3 {
margin: 10px 0 5px;
}
.user-card p {
color: #666;
font-size: 14px;
}
</style>
</head>
<body>
<div id="app">
<h2>团队成员</h2>
<user-card
v-for="user in users"
:key="user.id"
:name="user.name"
:email="user.email"
:avatar="user.avatar">
</user-card>
</div>

<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<script>
const { createApp } = Vue;

const app = createApp({
data() {
return {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com', avatar: 'https://i.pravatar.cc/150?img=1' },
{ id: 2, name: '李四', email: 'lisi@example.com', avatar: 'https://i.pravatar.cc/150?img=2' },
{ id: 3, name: '王五', email: 'wangwu@example.com', avatar: 'https://i.pravatar.cc/150?img=3' }
]
}
}
});

app.component('user-card', {
props: {
name: String,
email: String,
avatar: String
},
template: `
<div class="user-card">
<img :src="avatar" :alt="name">
<h3>{{ name }}</h3>
<p>{{ email }}</p>
</div>
`
});

app.mount('#app');
</script>
</body>
</html>

🎉 恭喜你完成第六节课!

下一节我们将学习:生命周期钩子 —— 了解组件从创建到销毁的整个过程,学会在合适的时机执行代码。