08.组件开发与UI框架
1. 组件通信机制:组件间的数据传递
1.1 为什么需要组件通信?
在第七章中,我们学习了Vue.js的基础组件概念。然而,在实际开发中,组件往往不是孤立存在的,它们需要相互协作、传递数据。
实际场景:
- 父组件需要向子组件传递数据(如用户信息)
- 子组件需要向父组件通知事件(如按钮点击)
- 兄弟组件之间需要共享数据(如购物车状态)
组件通信的挑战:
- 组件之间不能直接访问对方的数据(数据隔离)
- 需要明确的通信机制来传递数据和事件
- 不同层级的组件通信方式不同
1.2 父子组件通信:Props和Emit
1.2.1 Props:父组件向子组件传值
Props是父组件向子组件传递数据的方式。
基本语法: 1
2
3
4
5
6
7
8// 子组件:定义props
app.component('child-component', {
props: ['message'],
template: '<div>{{ message }}</div>'
});
// 父组件:传递数据
<child-component message="Hello"></child-component>
说明(最小必懂):
- 子组件用
props声明“我需要什么数据” - 父组件用
:属性名="数据"传递(带冒号表示绑定变量,而不是纯字符串) - 在子组件模板里直接用
{{ message }}渲染
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<div id="app">
<user-card
:name="user.name"
:age="user.age"
:email="user.email">
</user-card>
</div>
<script>
{{ name }}</h3>
{{ age }}</p>
{{ email }}</p>
</script>
Props的特点:
- 单向数据流:数据只能从父组件流向子组件
- 子组件不能直接修改props(会报错)
- 如果需要修改,应该通知父组件修改
1.2.2 Emit:子组件向父组件传值
Emit是子组件向父组件传递事件的方式。
基本语法: 1
2
3
4
5// 子组件:触发事件
this.$emit('事件名', 数据);
// 父组件:监听事件
<child-component @事件名="处理函数"></child-component>
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34<div id="app">
<p>计数:{{ count }}</p>
<counter-button @increment="handleIncrement"></counter-button>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
count: 0
}
},
methods: {
handleIncrement() {
this.count++;
}
}
});
app.component('counter-button', {
template: `
<button @click="increment">点击+1</button>
`,
methods: {
increment() {
this.$emit('increment');
}
}
});
app.mount('#app');
</script>
Emit的特点:
- 子组件通过事件通知父组件
- 父组件决定如何处理事件
- 可以实现数据的"向上传递"
说明(执行流程): 1)
子组件内部点击按钮,调用this.$emit('increment')发事件
2) 父组件在模板上用@increment="handleIncrement"监听
3)
父组件的handleIncrement里修改自己的count,触发视图更新
1.2.3 双向绑定:v-model的实现
v-model实际上是props和emit的语法糖。
v-model的原理: 1
2
3
4
5
6
7
8
9
10// 以下两种写法等价:
// 写法1:使用v-model
<custom-input v-model="value"></custom-input>
// 写法2:使用props和emit
<custom-input
:value="value"
@update:value="value = $event">
</custom-input>
实际例子:自定义输入框组件 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28<div id="app">
<p>输入的值:{{ inputValue }}</p>
<custom-input v-model="inputValue"></custom-input>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
inputValue: ''
}
}
});
app.component('custom-input', {
props: ['modelValue'],
template: `
<input
:value="modelValue"
@input="$emit('update:modelValue', $event.target.value)">
`,
emits: ['update:modelValue']
});
app.mount('#app');
</script>
说明(工作原理):
- 父组件用
v-model="inputValue"绑定数据 - 子组件接收
modelValue(固定命名)作为当前值 - 子组件输入变更时,发出
update:modelValue事件,把新值告诉父组件 - 父组件收到事件后更新
inputValue,从而完成双向同步
1.3 兄弟组件通信:通过父组件中转
问题:两个兄弟组件如何通信?
解决方案:通过父组件作为"中转站"
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41<div id="app">
<component-a @send-data="handleData"></component-a>
<component-b :data="sharedData"></component-b>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
sharedData: null
}
},
methods: {
handleData(data) {
this.sharedData = data;
}
}
});
app.component('component-a', {
template: `
<button @click="send">发送数据</button>
`,
methods: {
send() {
this.$emit('send-data', '这是来自A组件的数据');
}
}
});
app.component('component-b', {
props: ['data'],
template: `
<div>接收到的数据:{{ data }}</div>
`
});
app.mount('#app');
</script>
流程:
- 组件A通过emit发送数据到父组件
- 父组件接收数据并更新状态
- 父组件通过props将数据传递给组件B
核心记忆:兄弟组件不直接通信,永远“先上再下”——先发给父,再由父传给另一个子。
2. 组件传值实践:构建可复用组件
2.1 组件设计原则
2.1.1 单一职责原则
每个组件应该只负责一个功能,职责明确。
好的设计: 1
2
3
4
5
6
7
8
9
10
11// 用户头像组件:只负责显示头像
app.component('user-avatar', {
props: ['src', 'alt'],
template: '<img :src="src" :alt="alt">'
});
// 用户信息组件:只负责显示用户信息
app.component('user-info', {
props: ['name', 'email'],
template: '<div>{{ name }} - {{ email }}</div>'
});
不好的设计: 1
2
3
4// 一个组件做了太多事情
app.component('user-card', {
// 显示头像、信息、按钮、表单... 职责不清晰
});
2.1.2 可复用性
组件应该设计得足够通用,可以在不同场景下复用。
实际例子:按钮组件 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26app.component('custom-button', {
props: {
type: {
type: String,
default: 'primary' // 默认值
},
disabled: {
type: Boolean,
default: false
},
loading: {
type: Boolean,
default: false
}
},
template: `
<button
:class="['btn', 'btn-' + type]"
:disabled="disabled || loading"
@click="$emit('click')">
<span v-if="loading">加载中...</span>
<slot v-else></slot>
</button>
`,
emits: ['click']
});
使用方式: 1
2<custom-button type="primary" @click="handleClick">提交</custom-button>
<custom-button type="danger" :loading="isLoading">删除</custom-button>
2.2 实战案例:构建用户列表组件
2.2.1 需求分析
我们要构建一个用户列表组件,包含:
- 显示用户列表
- 支持搜索功能
- 支持删除用户
- 可复用、可配置
2.2.2 组件设计
用户列表组件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77<div id="app">
<user-list
:users="users"
@delete-user="handleDelete">
</user-list>
</div>
<script>
const { createApp } = Vue;
const app = createApp({
data() {
return {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' },
{ id: 3, name: '王五', email: 'wangwu@example.com' }
]
}
},
methods: {
handleDelete(userId) {
this.users = this.users.filter(u => u.id !== userId);
}
}
});
app.component('user-list', {
props: {
users: {
type: Array,
required: true
}
},
data() {
return {
searchKeyword: ''
}
},
computed: {
filteredUsers() {
if (!this.searchKeyword) {
return this.users;
}
return this.users.filter(user =>
user.name.includes(this.searchKeyword) ||
user.email.includes(this.searchKeyword)
);
}
},
template: `
<div class="user-list">
<input
v-model="searchKeyword"
placeholder="搜索用户..."
class="search-input">
<ul class="user-items">
<li v-for="user in filteredUsers" :key="user.id" class="user-item">
<div>
<strong>{{ user.name }}</strong>
<p>{{ user.email }}</p>
</div>
<button
@click="$emit('delete-user', user.id)"
class="delete-btn">
删除
</button>
</li>
</ul>
</div>
`,
emits: ['delete-user']
});
app.mount('#app');
</script>
代码解读(一步步看):
- 父组件把
users数组传给user-list - 组件内部用
searchKeyword做本地搜索状态 computed里的filteredUsers负责筛选(名字或邮箱包含关键字)- 模板里
v-for渲染列表,点击“删除”按钮用$emit('delete-user', id)通知父组件 - 父组件在
handleDelete里真正删除数据
2.3 Props验证和默认值
2.3.1 Props类型验证
语法: 1
2
3
4
5
6
7
8props: {
属性名: {
type: 类型,
required: true/false,
default: 默认值,
validator: 验证函数
}
}
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19app.component('user-card', {
props: {
name: {
type: String,
required: true
},
age: {
type: Number,
default: 0,
validator(value) {
return value >= 0 && value <= 150;
}
},
email: {
type: String,
default: ''
}
}
});
2.3.2 为什么需要Props验证?
好处:
- 提前发现错误(开发时就能发现问题)
- 代码更清晰(明确组件需要什么数据)
- 更好的开发体验(IDE可以提供类型提示)
3. UI框架应用:提升开发效率
3.1 为什么需要UI框架?
3.1.1 传统开发的痛点
在之前的章节中,我们使用原生CSS和HTML构建界面。这种方式存在以下问题:
问题:
- 需要从零开始写样式(按钮、表单、表格等)
- 样式不统一(每个页面风格可能不同)
- 开发效率低(重复造轮子)
- 响应式适配复杂(需要写大量媒体查询)
3.1.2 UI框架的解决方案
UI框架提供了一套预定义的组件和样式,帮助开发者快速构建界面。
| 传统方式 | UI框架方式 |
|---|---|
| 从零开始写样式 | 使用现成组件 |
| 样式不统一 | 统一的设计规范 |
| 开发效率低 | 快速开发 |
| 响应式复杂 | 内置响应式支持 |
3.2 Ant Design Vue:企业级UI框架
3.2.1 什么是Ant Design Vue?
Ant Design Vue是蚂蚁集团开源的企业级UI组件库,基于Vue 3开发。
特点:
- 组件丰富(60+组件)
- 设计规范统一
- 中文文档完善
- 企业级应用广泛使用
适合初学者的原因:有完善示例和文档,几乎所有基础UI(按钮、表格、表单、弹窗、栅格)都能直接用,减少CSS工作量。
3.2.2 安装和使用
安装: 1
npm install ant-design-vue
引入: 1
2
3
4
5
6
7import { createApp } from 'vue';
import Antd from 'ant-design-vue';
import 'ant-design-vue/dist/reset.css';
const app = createApp(App);
app.use(Antd);
app.mount('#app');
如果只是课堂练习,也可以用 CDN 方式快速体验(无需安装):
<script src="https://unpkg.com/ant-design-vue@next/dist/antd.min.js"></script>
3.2.3 常用组件示例
按钮组件: 1
2
3
4
5
6<template>
<a-button type="primary">主要按钮</a-button>
<a-button type="default">默认按钮</a-button>
<a-button type="dashed">虚线按钮</a-button>
<a-button danger>危险按钮</a-button>
</template>
要点:type控制样式,danger显示警示色;常用事件为@click。
表格组件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36<template>
<a-table :columns="columns" :data-source="data">
<template #bodyCell="{ column, record }">
<span v-if="column.key === 'action'">
<a-button @click="edit(record)">编辑</a-button>
<a-button danger @click="delete(record)">删除</a-button>
</span>
</template>
</a-table>
</template>
<script>
export default {
data() {
return {
columns: [
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '年龄', dataIndex: 'age', key: 'age' },
{ title: '操作', key: 'action' }
],
data: [
{ key: '1', name: '张三', age: 20 },
{ key: '2', name: '李四', age: 25 }
]
}
},
methods: {
edit(record) {
console.log('编辑', record);
},
delete(record) {
console.log('删除', record);
}
}
}
</script>
阅读提示:
- 表格:
columns描述列,data-source提供数据,#bodyCell自定义操作列 - 弹窗表单:
user-form-modal用v-model:visible控制显示,提交后通过@submit把表单数据传回父组件 - 逻辑分离:列表负责展示和触发编辑/删除;表单负责输入和校验;父组件负责数据增删改
要点:columns定义列头和字段,data-source提供数据;如果需要自定义单元格,用#bodyCell插槽判断column.key。
表单组件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39<template>
<a-form :model="form" @finish="onFinish">
<a-form-item
label="用户名"
name="username"
:rules="[{ required: true, message: '请输入用户名' }]">
<a-input v-model:value="form.username" />
</a-form-item>
<a-form-item
label="密码"
name="password"
:rules="[{ required: true, message: '请输入密码' }]">
<a-input-password v-model:value="form.password" />
</a-form-item>
<a-form-item>
<a-button type="primary" html-type="submit">提交</a-button>
</a-form-item>
</a-form>
</template>
<script>
export default {
data() {
return {
form: {
username: '',
password: ''
}
}
},
methods: {
onFinish(values) {
console.log('提交的数据:', values);
}
}
}
</script>
要点:rules里写校验规则(必填、邮箱格式等),提交时触发@finish,表单数据在values中。
3.3 UI框架的价值
3.3.1 提升开发效率
对比:
| 功能 | 传统方式 | UI框架方式 |
|---|---|---|
| 按钮 | 写CSS样式、处理状态 | 直接使用<a-button> |
| 表格 | 写HTML结构、处理排序分页 | 使用<a-table>,配置数据即可 |
| 表单验证 | 写JavaScript验证逻辑 | 使用<a-form>,配置规则即可 |
实际效果:
- 开发时间减少60%以上
- 代码量减少50%以上
- 维护成本降低
3.3.2 保证设计一致性
UI框架提供统一的设计规范:
- 颜色系统(主色、辅助色、语义色)
- 字体系统(字号、行高、字重)
- 间距系统(统一的间距规范)
- 组件样式(按钮、输入框、卡片等)
好处:
- 界面风格统一
- 用户体验一致
- 减少设计决策
3.3.3 响应式支持
UI框架内置响应式支持,无需手动写媒体查询。
实际例子: 1
2
3
4
5
6
7
8
9<!-- Ant Design Vue的栅格系统 -->
<a-row :gutter="16">
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<div class="card">卡片1</div>
</a-col>
<a-col :xs="24" :sm="12" :md="8" :lg="6">
<div class="card">卡片2</div>
</a-col>
</a-row>
效果:
- 手机(xs):每行1个
- 平板(sm):每行2个
- 电脑(md/lg):每行4个
4. AI实战:使用AI开发用户选择器
4.1 任务描述
目标:使用AI工具生成一个用户选择器组件
功能要求:
- 显示用户列表
- 支持搜索用户
- 支持多选
- 使用Ant Design Vue组件
- 可复用、可配置
4.2 第一步:设计Prompt
Prompt示例: 1
2
3
4
5
6
7
8用Vue 3和Ant Design Vue创建一个用户选择器组件,要求:
1. 使用Ant Design Vue的Select组件
2. 支持搜索功能
3. 支持多选模式
4. 用户数据通过props传入
5. 选择变化时通过emit通知父组件
6. 包含加载状态
7. 代码注释清晰,使用组合式API
4.3 AI生成的代码(示例)
用户选择器组件: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65<template>
<a-select
v-model:value="selectedUsers"
mode="multiple"
:options="filteredOptions"
:loading="loading"
:placeholder="placeholder"
:filter-option="filterOption"
@change="handleChange"
style="width: 100%">
</a-select>
</template>
<script>
import { ref, computed } from 'vue';
export default {
name: 'UserSelector',
props: {
users: {
type: Array,
required: true,
default: () => []
},
placeholder: {
type: String,
default: '请选择用户'
},
loading: {
type: Boolean,
default: false
}
},
emits: ['change'],
setup(props, { emit }) {
const selectedUsers = ref([]);
// 转换用户数据为Select组件需要的格式
const options = computed(() => {
return props.users.map(user => ({
label: `${user.name} (${user.email})`,
value: user.id
}));
});
// 过滤选项(搜索功能)
const filterOption = (input, option) => {
return option.label.toLowerCase().includes(input.toLowerCase());
};
// 处理选择变化
const handleChange = (value) => {
selectedUsers.value = value;
emit('change', value);
};
return {
selectedUsers,
options,
filterOption,
handleChange
};
}
}
</script>
使用方式: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31<template>
<user-selector
:users="users"
:loading="isLoading"
@change="handleUserChange">
</user-selector>
</template>
<script>
import UserSelector from './components/UserSelector.vue';
export default {
components: {
UserSelector
},
data() {
return {
users: [
{ id: 1, name: '张三', email: 'zhangsan@example.com' },
{ id: 2, name: '李四', email: 'lisi@example.com' }
],
isLoading: false
}
},
methods: {
handleUserChange(selectedIds) {
console.log('选中的用户ID:', selectedIds);
}
}
}
</script>
代码要点(初学者友好版):
props.users提供用户数据,组件内部用computed options把它转换成Select需要的label/value结构mode="multiple"开启多选,v-model:value双向绑定选中值filter-option实现搜索(忽略大小写)- 选择变化时触发
@change,通过emit('change', value)把结果告诉父组件
4.4 代码解析
核心思路:
- 使用Ant Design Vue的
<a-select>组件 - 通过props接收用户数据
- 使用computed转换数据格式
- 通过emit通知父组件选择变化
- 支持搜索和多选功能
关键点:
- 组件设计遵循单一职责原则
- 通过props和emit实现组件通信
- 使用UI框架提升开发效率
5. 实战案例:实现CRUD界面
5.1 需求分析
我们要实现一个用户管理的CRUD界面:
- Create:创建用户
- Read:查看用户列表
- Update:更新用户信息
- Delete:删除用户
5.2 组件设计
5.2.1 用户列表组件
1 | <template> |
5.2.2 用户表单组件
1 | <template> |
阅读提示:
v-model:visible控制弹窗显示/隐藏rules定义表单校验(必填、邮箱格式、年龄范围)watch监听传入的user,决定是“新增”还是“编辑”并填充表单- 提交时先校验(
formRef.validate()),通过emit('submit', form)把数据传回父组件
5.3 代码解析
核心思路:
- 使用Ant Design Vue的表格和表单组件
- 通过props和emit实现组件通信
- 使用v-model实现双向绑定
- 表单验证使用Ant Design Vue的规则系统
关键点:
- 组件职责分离(列表组件、表单组件)
- 通过事件实现组件通信
- 使用UI框架提升开发效率
6. 选讲:动态组件与插槽
6.1 动态组件
6.1.1 什么是动态组件?
动态组件允许根据条件动态切换不同的组件。
语法:<component :is="组件名">
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24<template>
<div>
<button @click="currentTab = 'tab1'">标签1</button>
<button @click="currentTab = 'tab2'">标签2</button>
<button @click="currentTab = 'tab3'">标签3</button>
<component :is="currentTab"></component>
</div>
</template>
<script>
export default {
data() {
return {
currentTab: 'tab1'
}
},
components: {
tab1: { template: '<div>标签1的内容</div>' },
tab2: { template: '<div>标签2的内容</div>' },
tab3: { template: '<div>标签3的内容</div>' }
}
}
</script>
6.1.2 动态组件的应用场景
常见场景:
- 标签页切换
- 条件渲染不同组件
- 路由组件切换
6.2 插槽(Slot)
6.2.1 为什么需要插槽?
问题场景:假设我们要创建一个卡片组件,但不同地方使用的卡片内容完全不同。
没有插槽的问题: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17<!-- 方式1:写死内容,无法复用 -->
<template>
<div class="card">
<h3>用户信息</h3>
<p>张三</p>
</div>
</template>
<!-- 问题:只能显示"用户信息",其他地方想显示"商品信息"就不行了 -->
<!-- 方式2:用props传递,但很麻烦 -->
<template>
<div class="card">
<h3>{{ title }}</h3>
<p>{{ content }}</p>
</div>
</template>
<!-- 问题:如果内容很复杂(包含多个元素、按钮、图片等),用props传递会很麻烦 -->
插槽的解决方案: 插槽允许父组件把任意内容(HTML、组件等)传递给子组件,子组件在指定位置显示这些内容。
类比理解: - Props:像传参数,只能传数据 - 插槽:像传"模板",可以传完整的HTML结构
6.2.2 普通插槽(默认插槽)
基本概念:
普通插槽是最简单的插槽,子组件用<slot>标记一个位置,父组件传入的内容会显示在这个位置。
完整例子: 1
2
3
4
5
6
7
8
9<!-- 子组件:Card.vue -->
<template>
<div class="card" style="border: 1px solid #ccc; padding: 20px; margin: 10px;">
<h3>卡片标题</h3>
<!-- slot标记:这里会显示父组件传入的内容 -->
<slot></slot>
<p style="color: #999; font-size: 12px;">卡片底部</p>
</div>
</template>
1 | <!-- 父组件:App.vue --> |
运行效果: - 第一张卡片:显示"这是第一张卡片的内容"和按钮 - 第二张卡片:显示图片和文字 - 两张卡片都有相同的"卡片标题"和"卡片底部",但中间内容不同
代码解读: 1.
子组件:<slot></slot>是一个占位符,表示"这里会显示父组件传入的内容"
2.
父组件:在<card>标签内部写的内容,会自动替换子组件中的<slot>
3. 灵活性:同一个Card组件,可以显示完全不同的内容
默认内容:
如果父组件没有传入内容,可以给插槽设置默认内容: 1
2
3
4
5
6
7
8
9<!-- 子组件 -->
<template>
<div class="card">
<slot>
<!-- 默认内容:如果父组件没传内容,就显示这个 -->
<p>暂无内容</p>
</slot>
</div>
</template>
6.2.3 具名插槽(Named Slots)
为什么需要具名插槽? 普通插槽只能有一个位置,但如果子组件有多个位置需要插入内容(比如头部、主体、底部),就需要具名插槽。
基本概念:
具名插槽通过name属性区分不同的插槽位置,父组件通过#插槽名或v-slot:插槽名指定内容插入到哪个位置。
完整例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19<!-- 子组件:Layout.vue -->
<template>
<div class="layout">
<!-- 头部插槽:name="header" -->
<header style="background: #f0f0f0; padding: 10px;">
<slot name="header"></slot>
</header>
<!-- 主体插槽:没有name,是默认插槽 -->
<main style="padding: 20px; min-height: 200px;">
<slot></slot>
</main>
<!-- 底部插槽:name="footer" -->
<footer style="background: #f0f0f0; padding: 10px; text-align: center;">
<slot name="footer"></slot>
</footer>
</div>
</template>
1 | <!-- 父组件:App.vue --> |
代码解读: 1. 子组件: -
<slot name="header">:头部插槽,name是"header" -
<slot>:默认插槽,没有name(或name="default") -
<slot name="footer">:底部插槽,name是"footer"
- 父组件:
<template #header>:#是v-slot:的简写,表示内容插入到name="header"的插槽- 直接写的内容(没有template包裹):插入到默认插槽
<template v-slot:footer>:完整写法,效果和#footer一样
- 执行流程:
- Vue找到
#header的内容,放到<slot name="header">位置 - Vue找到默认内容,放到
<slot>位置 - Vue找到
#footer的内容,放到<slot name="footer">位置
- Vue找到
语法说明: - #header 是
v-slot:header 的简写 - #default
是默认插槽的完整写法(通常省略) -
多个具名插槽可以按任意顺序写,Vue会根据name自动匹配
6.2.4 作用域插槽(Scoped Slots)
为什么需要作用域插槽? 有时候,子组件需要向插槽传递数据,让父组件可以使用这些数据来渲染内容。
问题场景: 子组件有一个列表,但父组件想自定义每个列表项的显示方式。
没有作用域插槽的问题: 1
2
3
4
5
6
7
8
9<!-- 子组件 -->
<template>
<ul>
<li v-for="item in items" :key="item.id">
<!-- 问题:这里写死了显示方式,父组件无法自定义 -->
{{ item.name }}
</li>
</ul>
</template>
作用域插槽的解决方案:
子组件通过<slot>的绑定属性传递数据,父组件接收这些数据并自定义显示方式。
完整例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20<!-- 子组件:UserList.vue -->
<template>
<ul>
<li v-for="user in users" :key="user.id" style="padding: 10px; border-bottom: 1px solid #eee;">
<!-- 通过 :user="user" 把当前循环的user数据传递给插槽 -->
<slot :user="user" :index="index"></slot>
</li>
</ul>
</template>
<script>
export default {
props: {
users: {
type: Array,
required: true
}
}
}
</script>
1 | <!-- 父组件:App.vue --> |
代码解读: 1. 子组件: -
<slot :user="user">:通过:user="user"把数据绑定到slot上
- 这个绑定的数据会传递给父组件
- 父组件:
#default="{ user }":接收slot传递的数据,解构出user属性#default="slotProps":完整写法,slotProps是一个对象,包含所有绑定的数据- 父组件可以用这些数据自定义显示方式
- 数据流向:
- 子组件 → 插槽绑定 → 父组件接收 → 自定义渲染
- 子组件负责提供数据和结构,父组件负责决定如何显示
传递多个数据: 1
2
3
4
5
6
7
8
9
10<!-- 子组件 -->
<slot :user="user" :index="index" :isLast="index === items.length - 1"></slot>
<!-- 父组件 -->
<template #default="{ user, index, isLast }">
<div>
<span>第{{ index + 1 }}项:{{ user.name }}</span>
<span v-if="isLast">(最后一项)</span>
</div>
</template>
实际应用场景: - 表格组件:子组件提供数据,父组件自定义每列的显示方式 - 列表组件:子组件提供列表项数据,父组件自定义每个项的样式 - 卡片组件:子组件提供数据,父组件决定如何展示
6.3 插槽的实际应用
6.3.1 表格组件中的插槽
实际场景:使用Ant Design Vue的表格组件时,我们想自定义某些列的显示方式(比如状态显示为标签,操作列显示按钮)。
完整例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52<template>
<a-table :columns="columns" :data-source="data">
<!-- bodyCell是Ant Design Vue提供的插槽,用于自定义单元格内容 -->
<template #bodyCell="{ column, record }">
<!-- column:当前列的配置信息(包含key、title等) -->
<!-- record:当前行的数据对象 -->
<!-- 如果当前列是status列,显示为标签 -->
<template v-if="column.key === 'status'">
<a-tag :color="record.status === 'active' ? 'green' : 'red'">
{{ record.status === 'active' ? '激活' : '禁用' }}
</a-tag>
</template>
<!-- 如果当前列是action列,显示操作按钮 -->
<template v-else-if="column.key === 'action'">
<a-button @click="edit(record)" style="margin-right: 8px;">编辑</a-button>
<a-button danger @click="delete(record)">删除</a-button>
</template>
<!-- 其他列使用默认显示方式 -->
</template>
</a-table>
</template>
<script>
export default {
data() {
return {
columns: [
{ title: 'ID', dataIndex: 'id', key: 'id' },
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '状态', dataIndex: 'status', key: 'status' },
{ title: '操作', key: 'action' } // 注意:操作列没有dataIndex
],
data: [
{ id: 1, name: '张三', status: 'active' },
{ id: 2, name: '李四', status: 'inactive' },
{ id: 3, name: '王五', status: 'active' }
]
}
},
methods: {
edit(record) {
console.log('编辑:', record);
},
delete(record) {
console.log('删除:', record);
}
}
}
</script>
代码解读: 1.
#bodyCell插槽: - 这是Ant Design
Vue提供的插槽,用于自定义表格单元格的显示 -
每个单元格都会调用这个插槽,传入column(列信息)和record(行数据)
column.key的作用:- 通过
column.key判断当前是哪个列 - 在columns配置中,每个列都有一个
key属性
- 通过
record的作用:record是当前行的完整数据对象- 可以通过
record.id、record.name等访问该行的所有数据
- 执行流程:
- 表格渲染每一行时,对每个单元格调用
bodyCell插槽 - 根据
column.key判断是哪个列,决定显示什么内容 - status列:显示为带颜色的标签
- action列:显示编辑和删除按钮
- 其他列:使用默认显示(直接显示
dataIndex对应的值)
- 表格渲染每一行时,对每个单元格调用
更复杂的例子:自定义头像列 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66<template>
<a-table :columns="columns" :data-source="data">
<template #bodyCell="{ column, record }">
<!-- 头像列:显示图片 -->
<template v-if="column.key === 'avatar'">
<img :src="record.avatar" :alt="record.name" style="width: 40px; height: 40px; border-radius: 50%;">
</template>
<!-- 状态列:根据状态显示不同颜色 -->
<template v-else-if="column.key === 'status'">
<a-tag :color="getStatusColor(record.status)">
{{ getStatusText(record.status) }}
</a-tag>
</template>
<!-- 操作列:多个按钮 -->
<template v-else-if="column.key === 'action'">
<a-space>
<a-button size="small" @click="view(record)">查看</a-button>
<a-button size="small" type="primary" @click="edit(record)">编辑</a-button>
<a-button size="small" danger @click="delete(record)">删除</a-button>
</a-space>
</template>
</template>
</a-table>
</template>
<script>
export default {
data() {
return {
columns: [
{ title: '头像', key: 'avatar' },
{ title: '姓名', dataIndex: 'name', key: 'name' },
{ title: '状态', key: 'status' },
{ title: '操作', key: 'action' }
],
data: [
{ id: 1, name: '张三', avatar: '/avatar1.jpg', status: 'active' },
{ id: 2, name: '李四', avatar: '/avatar2.jpg', status: 'pending' }
]
}
},
methods: {
getStatusColor(status) {
const colors = {
active: 'green',
pending: 'orange',
inactive: 'red'
};
return colors[status] || 'default';
},
getStatusText(status) {
const texts = {
active: '激活',
pending: '待审核',
inactive: '禁用'
};
return texts[status] || status;
},
view(record) { /* ... */ },
edit(record) { /* ... */ },
delete(record) { /* ... */ }
}
}
</script>
关键要点: - 插槽让表格组件变得非常灵活,可以自定义任何列的显示方式 - 不需要修改表格组件本身的代码,只需要在使用时通过插槽自定义 - 这是组件设计"可扩展性"的体现
6.3.2 插槽总结
三种插槽对比:
| 插槽类型 | 使用场景 | 语法特点 | 数据流向 |
|---|---|---|---|
| 普通插槽 | 只有一个位置需要插入内容 | <slot></slot> |
父组件 → 子组件 |
| 具名插槽 | 多个位置需要插入不同内容 | <slot name="xxx"> + #xxx |
父组件 → 子组件(多个位置) |
| 作用域插槽 | 子组件需要向父组件传递数据 | <slot :data="data"> +
#default="{ data }" |
子组件 → 父组件(数据) |
记忆要点:
普通插槽:最简单的插槽,父组件传内容,子组件显示
1
2
3
4
5<!-- 子组件 -->
<slot></slot>
<!-- 父组件 -->
<my-component>内容</my-component>具名插槽:多个位置,用name区分
1
2
3
4
5
6
7
8
9<!-- 子组件 -->
<slot name="header"></slot>
<slot name="footer"></slot>
<!-- 父组件 -->
<my-component>
<template #header>头部</template>
<template #footer>底部</template>
</my-component>作用域插槽:子组件传数据给父组件
1
2
3
4
5
6
7<!-- 子组件 -->
<slot :item="item"></slot>
<!-- 父组件 -->
<my-component>
<template #default="{ item }">{{ item.name }}</template>
</my-component>
什么时候用插槽?
- ✅ 需要让组件内容可自定义时
- ✅ 组件结构固定,但内容变化时
- ✅ 需要子组件向父组件传递数据来渲染时
什么时候不用插槽?
- ❌ 内容完全固定,不需要自定义 → 直接写在组件里
- ❌ 只需要传递简单数据 → 用props
- ❌ 只需要通知事件 → 用emit
实际开发建议:
- 先思考:这个内容是否需要父组件自定义?
- 需要 → 用插槽
- 不需要 → 直接写死或 props
- 再判断:需要几个位置?
- 1个位置 → 普通插槽
- 多个位置 → 具名插槽
- 最后考虑:子组件是否需要传递数据?
- 需要 → 作用域插槽
- 不需要 → 普通/具名插槽
7. 组件设计模式
7.1 容器组件与展示组件
7.1.1 设计模式
容器组件(Container Component):
- 负责数据获取和状态管理
- 处理业务逻辑
- 通过props向展示组件传递数据
展示组件(Presentational Component):
- 只负责UI渲染
- 通过props接收数据
- 通过emit发送事件
实际例子: 1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52<!-- 容器组件:UserListContainer.vue -->
<template>
<user-list
:users="users"
:loading="loading"
@delete="handleDelete"
@refresh="loadUsers">
</user-list>
</template>
<script>
export default {
data() {
return {
users: [],
loading: false
}
},
mounted() {
this.loadUsers();
},
methods: {
async loadUsers() {
this.loading = true;
// 从API获取数据
this.users = await fetchUsers();
this.loading = false;
},
handleDelete(id) {
// 删除用户逻辑
}
}
}
</script>
<!-- 展示组件:UserList.vue -->
<template>
<div v-if="loading">加载中...</div>
<ul v-else>
<li v-for="user in users" :key="user.id">
{{ user.name }}
<button @click="$emit('delete', user.id)">删除</button>
</li>
</ul>
</template>
<script>
export default {
props: ['users', 'loading'],
emits: ['delete']
}
</script>
7.1.2 这种模式的优势
| 优势 | 说明 |
|---|---|
| 职责分离 | 容器组件处理逻辑,展示组件处理UI |
| 可复用性 | 展示组件可以在不同场景复用 |
| 易测试 | 展示组件易于单元测试 |
| 易维护 | 逻辑和UI分离,维护更方便 |
7.2 组件组合模式
7.2.1 组合优于继承
在组件设计中,应该通过组合小组件来构建大组件,而不是通过继承。
好的设计(组合): 1
2
3
4
5
6
7<template>
<div class="user-profile">
<user-avatar :src="user.avatar" />
<user-info :name="user.name" :email="user.email" />
<user-actions :userId="user.id" />
</div>
</template>
不好的设计(继承): 1
<!-- 试图通过继承扩展组件,但Vue不支持类继承 -->
8. 本节总结
你已经学会了:
✅ 组件通信机制:
- Props:父组件向子组件传值
- Emit:子组件向父组件传值
- 双向绑定:v-model的实现原理
✅ 组件传值实践:
- 组件设计原则(单一职责、可复用性)
- Props验证和默认值
- 构建可复用组件
✅ UI框架应用:
- Ant Design Vue的使用
- UI框架的价值(提升效率、保证一致性)
- 响应式支持
✅ 实战技能:
- 使用AI生成组件
- 实现CRUD界面
- 组件设计模式
✅ 进阶知识(选讲):
- 动态组件
- 插槽(普通插槽、具名插槽、作用域插槽)
组件化和UI框架是现代前端开发的核心。掌握组件通信机制、设计模式和UI框架的使用,能够显著提升开发效率和代码质量。建议通过实际项目练习,逐步熟悉组件设计和UI框架的应用。