Vue组件
# Vue组件
# 1. 什么是组件化开发
组件化开发指的是:根据封装的思想,把页面上可重用的 UI 结构封装为组件,从而方便项目的开发和维护。
组件化开发的核心思想是将页面划分为多个独立的、可复用的部分,每个部分都是一个组件。这些组件可以单独开发、测试和维护,最后通过组合这些组件来构建整个应用。
# 2. Vue中的组件化开发
Vue 是一个支持组件化开发的前端框架。Vue 中规定:组件的文件后缀名是 .vue
。在之前的例子中,我们已经接触到的 App.vue
文件本质上就是一个 Vue 的组件。
# 3. Vue组件的三个组成部分
每个 .vue
组件文件通常由三个部分组成,分别是:
- template:组件的模板结构
- script:组件的 JavaScript 行为
- style:组件的样式
其中,组件中必须包含 template
模板结构,而 script
和 style
是可选的部分。
# 3-1 template
Vue 规定:每个组件的模板结构需要定义在 <template>
标签内。
<template>
<div class="my-component">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
2
3
4
5
6
注意:
<template>
是 Vue 提供的容器标签,只起到包裹性质的作用,它不会被渲染为真正的 DOM 元素。<template>
中只能包含唯一的根节点,这意味着<template>
标签内部的所有内容必须嵌套在一个唯一的根元素中,通常我们会使用一个<div>
标签来包裹所有的内容。
# 3-2 script
Vue 规定:开发者可以在 <script>
标签内封装组件的 JavaScript 业务逻辑。
<template>
<div class="my-component">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
title: '这是一个标题',
description: '这是描述内容'
};
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
.vue 组件中的 data
必须是函数
Vue 规定:.vue 组件中的 data
必须是一个函数,不能直接指向一个数据对象。这样做是为了确保每个组件实例都有独立的数据副本,而不会共享同一个数据对象。
错误示例:
data: {
return {
title: '这是一个标题',
description: '这是描述内容'
};
}
2
3
4
5
6
这种写法会导致多个组件实例共用同一份数据,产生数据污染问题。
正确示例:
data() {
return {
title: '这是一个标题',
description: '这是描述内容'
};
}
2
3
4
5
6
# 3-3 style
Vue 规定:组件内的 <style>
标签是可选的,开发者可以在 <style>
标签内编写样式来美化当前组件的 UI 结构。
<template>
<div class="my-component">
<h1>{{ title }}</h1>
<p>{{ description }}</p>
</div>
</template>
<script>
export default {
name: 'MyComponent',
data() {
return {
title: '这是一个标题',
description: '这是描述内容'
};
}
};
</script>
<style scoped>
.my-component {
padding: 20px;
background-color: #f0f0f0;
}
.my-component h1 {
color: #333;
}
.my-component p {
color: #666;
}
</style>
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
<style scoped>
表示样式只作用于当前组件,防止样式污染全局。
# 4. 组件之间的父子关系
在 Vue 中,组件可以嵌套使用,从而形成父子关系。父组件可以通过 props
向子组件传递数据,子组件可以通过 $emit
向父组件发送消息。
示例代码:
<!-- 父组件 ParentComponent.vue -->
<template>
<div>
<h1>父组件</h1>
<child-component :message="parentMessage" @child-event="handleChildEvent"></child-component>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue';
export default {
name: 'ParentComponent',
components: {
ChildComponent
},
data() {
return {
parentMessage: '来自父组件的消息'
};
},
methods: {
handleChildEvent(data) {
console.log('收到子组件的消息:', data);
}
}
};
</script>
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
<!-- 子组件 ChildComponent.vue -->
<template>
<div>
<h2>子组件</h2>
<p>{{ message }}</p>
<button @click="sendMessageToParent">发送消息给父组件</button>
</div>
</template>
<script>
export default {
name: 'ChildComponent',
props: ['message'],
methods: {
sendMessageToParent() {
this.$emit('child-event', '这是子组件的消息');
}
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
在这个示例中:
- 父组件
ParentComponent
向子组件ChildComponent
传递了一个message
属性。 - 子组件
ChildComponent
可以使用props
接收到message
,并通过$emit
发送事件child-event
给父组件。 - 父组件通过
@child-event
监听子组件的事件,并调用handleChildEvent
方法处理事件。
# 5. 使用组件的三个步骤
使用 Vue 组件的步骤主要包括定义组件、注册组件和使用组件。具体步骤如下:
- 定义组件:创建一个
.vue
文件,定义组件的模板、逻辑和样式。 - 注册组件:在父组件或 Vue 实例中注册子组件,可以是私有注册或全局注册。
- 使用组件:在模板中使用自定义标签来引入子组件。
# 5-1 定义组件
创建一个 .vue
文件,定义组件的模板、逻辑和样式。
<!-- ChildComponent.vue -->
<template>
<div class="child-component">
<h2>子组件</h2>
</div>
</template>
<script>
export default {
name: 'ChildComponent', // 组件名
};
</script>
<style scoped>
.child-component {
color: blue; /* 设置组件的样式 */
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
在这个示例中,我们定义了一个子组件 ChildComponent
。
# 5-2 注册组件 (私有注册)
在父组件的 components
选项中注册子组件,这样子组件只能在当前父组件中使用。
<!-- ParentComponent.vue -->
<template>
<div>
<h1>父组件</h1>
<child-component></child-component> <!-- 使用子组件 -->
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'; // 导入子组件
export default {
name: 'ParentComponent', // 组件名
components: {
ChildComponent, // 私有注册子组件
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 5-3 使用组件
在模板中使用自定义标签来引入子组件。
<!-- ParentComponent.vue -->
<template>
<div>
<h1>父组件</h1>
<child-component></child-component> <!-- 使用子组件 -->
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'; // 导入子组件
export default {
name: 'ParentComponent', // 组件名
components: {
ChildComponent, // 私有注册子组件
},
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
# 6. 通过 components
注册的是私有子组件
在 Vue 组件的 components
选项中注册的子组件是私有的,仅在当前组件中可用。例如:
export default {
name: 'ParentComponent', // 组件名
components: {
ChildComponent // 私有子组件,只能在 ParentComponent 中使用
}
};
2
3
4
5
6
在这个例子中,ChildComponent
只能在 ParentComponent
中使用,不能在其他组件中使用。
# 7. 注册全局组件
在 Vue 项目的 main.js
入口文件中,通过 Vue.component()
方法可以注册全局组件。全局组件可以在任何组件中使用。
示例代码:
// main.js
import Vue from 'vue'; // 导入 Vue
import App from './App.vue'; // 导入主组件 App
import GlobalComponent from './components/GlobalComponent.vue'; // 导入全局组件
// 注册全局组件
Vue.component('global-component', GlobalComponent);
new Vue({
render: h => h(App), // 渲染主组件 App
}).$mount('#app'); // 挂载 Vue 实例到 id 为 app 的元素上
2
3
4
5
6
7
8
9
10
11
在 main.js
中注册的全局组件 GlobalComponent
可以在任何地方使用,无需导入。
<!-- 使用全局组件 -->
<template>
<div>
<global-component></global-component> <!-- 使用全局组件 -->
</div>
</template>
<script>
export default {
name: 'App', // 组件名
};
</script>
2
3
4
5
6
7
8
9
10
11
12
在这个示例中,GlobalComponent
被注册为全局组件,可以在任何组件中使用。这样做可以提高组件的复用性,方便在项目的不同部分使用相同的组件。
# 8. 组件的 props
props
是在子组件中定义的自定义属性,用于接收父组件传递的数据。父组件在使用子组件时,通过绑定属性的方式向子组件传递数据。通过 props
,父组件可以向子组件传递数据。props
的定义有两种语法格式:使用数组和使用对象。
# 8-1 props 的定义
使用数组定义 props
数组形式适合简单情况,只需要声明 props
名称。
export default {
// 使用数组定义 props
props: ['propName'] // 这里的 propName 是自定义属性名
}
2
3
4
使用对象定义 props
对象形式更为灵活,适合当你需要指定 props
的类型、默认值、是否必需等。
export default {
// 使用对象定义 props
props: {
propName: {
type: String,
required: true,
default: '默认值'
}
}
}
2
3
4
5
6
7
8
9
10
提示
在 Vue 中,props
可以定义的类型包括:String
(字符串)、Number
(数字)、Boolean
(布尔值)、Array
(数组)、Object
(对象)、Function
(函数)和任意类型 Any
(可以是多种类型的组合)。
在 Vue.js 中,给子组件传递数据时,通常使用属性绑定 (:
) 的方式来传递数据。这是因为属性绑定允许动态地将父组件的数据传递给子组件,从而在父组件的数据变化时自动更新子组件的显示内容。
# 8-2 props 的数据传递
使用属性绑定传递数据
属性绑定通过 :
语法实现,它会动态地将父组件的数据作为属性值传递给子组件:
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 使用子组件并通过 :message="parentMessage" 传递数据 -->
<ChildComponent :message="parentMessage"></ChildComponent>
</div>
</template>
2
3
4
5
6
7
在上面的例子中,parentMessage
是父组件中的一个响应式数据,它通过 :message="parentMessage"
的方式传递给子组件的 props
。
使用静态字符串传递数据
如果传递的是一个静态字符串常量,而不是响应式数据,可以不使用属性绑定,直接传递字符串值:
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 直接传递一个静态字符串 -->
<ChildComponent message="Static message"></ChildComponent>
</div>
</template>
2
3
4
5
6
7
在这种情况下,子组件接收到的是一个固定不变的字符串值 Static message
。这种方法适用于需要传递固定值的场景。
总结
- 动态数据:使用
:
绑定动态数据,使得子组件能够响应父组件的数据变化。 - 静态数据:直接传递字符串常量,无需使用
:
。
# 8-3 props 使用示例
在父组件中定义一个子组件并传递数据 (这里使用数组定义 props
):
<!-- ParentComponent.vue -->
<template>
<div>
<!-- 使用子组件并通过 :message="parentMessage" 传递数据 -->
<ChildComponent :message="parentMessage"></ChildComponent>
</div>
</template>
<script>
import ChildComponent from './ChildComponent.vue'; // 导入子组件
export default {
components: {
ChildComponent // 注册子组件
},
data() {
return {
parentMessage: 'Hello from Parent' // 定义父组件中的数据
};
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
在子组件中接收 props
:
<!-- ChildComponent.vue -->
<template>
<div>
<!-- 使用 props 中的数据 -->
<p>{{ message }}</p>
</div>
</template>
<script>
export default {
// 定义 props,用于接收父组件传递的数据
props: ['message']
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
注意无论你使用哪种方式定义 props
,在父组件中传递 props
的方法都是一样的。
# 9. props 是只读的
Vue 规定:组件中封装的自定义属性是只读的,程序员不能直接修改 props
的值,否则会报错。
示例
export default {
props: ['message'],
mounted() {
this.message = 'New Message'; // 试图修改 props 的值,会报错
}
};
2
3
4
5
6
要想修改 props
的值,可以将 props
的值转存到 data
中,因为 data
中的数据是可读可写的。
示例
<template>
<div>
<p>{{ localMessage }}</p> <!-- 使用 data 中的值 -->
</div>
</template>
<script>
export default {
props: ['message'],
data() {
return {
localMessage: this.message // 将 props 的值转存到 data 中
};
},
mounted() {
this.localMessage = 'New Message'; // 可以修改 data 中的值
}
};
</script>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
在这个例子中,我们将 props
的值 message
转存到 data
中的 localMessage
,然后可以自由地修改 localMessage
。
# 10. props 的 default 默认值
在声明自定义属性时,可以通过 default
来定义属性的默认值。
示例
export default {
props: {
message: {
type: String,
default: 'Default Message' // 定义默认值
}
}
};
2
3
4
5
6
7
8
在这个例子中,如果父组件没有传递 message
属性,子组件会使用默认值 'Default Message'
。
# 11. props 的 type 值类型
在声明自定义属性时,可以通过 type
来定义属性的值类型。
示例
export default {
props: {
message: {
type: String, // 定义属性的类型
default: 'Default Message'
},
count: {
type: Number, // 定义属性的类型
default: 0
}
}
};
2
3
4
5
6
7
8
9
10
11
12
在这个例子中,message
必须是一个字符串,count
必须是一个数字。
# 12. props 的 required 必填项
在声明自定义属性时,可以通过 required
选项,将属性设置为必填项,强制用户必须传递属性的值。
示例
export default {
props: {
message: {
type: String,
required: true // 定义必填项
}
}
};
2
3
4
5
6
7
8
在这个例子中,如果父组件没有传递 message
属性,Vue 会发出警告,因为 message
被标记为必填项。
# 13. 组件之间的样式冲突问题
默认情况下,写在 .vue
组件中的样式会全局生效,因此很容易造成多个组件之间的样式冲突问题。导致组件之间样式冲突的根本原因是:
- 单页面应用程序中,所有组件的 DOM 结构,都是基于唯一的
index.html
页面进行呈现的。 - 每个组件中的样式,都会影响整个
index.html
页面中的 DOM 元素。
# 14. 解决组件样式冲突的问题
为了解决样式冲突问题,可以为每个组件分配唯一的自定义属性,并在编写组件样式时,通过属性选择器来控制样式的作用域。示例代码如下:
<template>
<div class="component" my-unique-attribute>
<!-- 组件的内容 -->
</div>
</template>
<style>
/* 使用属性选择器限定样式作用域 */
[my-unique-attribute] .component {
color: red; /* 这个样式只会作用于带有 my-unique-attribute 属性的元素 */
}
</style>
2
3
4
5
6
7
8
9
10
11
12
# 15. style节点的 scoped属性
Vue.js 提供了 scoped
属性用于 <style>
标签中,以确保样式仅在当前组件的范围内生效。这是一个用于解决组件之间样式冲突的功能,特别是在组件化开发中非常有用。
- 作用:
scoped
属性用于将 CSS 样式限定在当前组件内,防止组件之间的样式相互影响。 - 工作原理:当一个
<style>
标签使用scoped
属性时,Vue 会为该组件生成一个唯一的属性(如data-v-xxxxxxx
),这个属性会附加到组件的根元素及其子元素上,用于标识该组件的样式作用域。 - 效果:使得 CSS 规则只作用于特定的组件,避免全局样式的污染。
下面是一个使用 scoped
属性的基本示例:
<template>
<div class="component">
<!-- 组件的内容 -->
<p>This is a component-specific paragraph.</p>
</div>
</template>
<style scoped>
.component {
color: red; /* 这个样式只会作用于当前组件的 .component 类 */
}
p {
font-size: 16px; /* 这个样式只会作用于当前组件内的 p 元素 */
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
# 15-1 浏览器中查看效果
当你在浏览器的开发者工具中查看组件元素时,会看到每个元素上都有一个自动生成的 data-v-xxxxxxx
属性:
<div class="component" data-v-3c83f0b7>
<p data-v-3c83f0b7>This is a component-specific paragraph.</p>
</div>
2
3
scoped 的局限性
- 不支持跨组件样式:
scoped
样式不能直接影响子组件的样式。如果需要对子组件进行样式覆盖,可以使用/deep/
或::v-deep
(Vue 3)选择器。 - 动态内容的样式应用:对于动态插入的内容,确保它们也带有正确的
data-v-xxxxxxx
属性,否则样式可能不生效。
# 15-2 data-v-xxxxxxx
属性的行为
组件中的 data-v-xxxxxxx
属性
唯一性:每个组件实例在编译时都会生成一个唯一的
data-v-xxxxxxx
标识符,这个标识符会附加到组件的根元素及其子元素上,用于标识该组件的样式作用域。一致性:同一个组件内部的所有元素会使用相同的
data-v-xxxxxxx
属性,以确保组件内样式的作用域一致。样式隔离:通过这种机制,Vue.js 实现了组件样式的隔离,确保一个组件的样式不会影响到其他组件,除非有明确的样式穿透(如
/deep/
)操作。
嵌套组件中的 data-v-xxxxxxx
属性
父组件样式作用:当一个父组件中嵌套了一个子组件,父组件的
data-v-xxxxxxx
属性也会附加到子组件的根元素上。这使得父组件定义的样式可以影响子组件的外观。子组件独立样式:子组件会生成自己的
data-v-xxxxxxx
属性,用于确保子组件的样式仅在其内部生效。
假设有以下组件结构:
ParentComponent
有一个data-v-aaaaaaa
标识符。ChildComponent
有一个data-v-bbbbbbb
标识符。
渲染的 DOM 会如下所示:
<div class="parent" data-v-aaaaaaa>
<div class="child" data-v-aaaaaaa data-v-bbbbbbb>
<p data-v-bbbbbbb>Child component content</p>
</div>
</div>
2
3
4
5
- 父组件样式:可以通过
[data-v-aaaaaaa] .child { /* styles */ }
作用于ChildComponent
。 - 子组件样式:通过
[data-v-bbbbbbb] .child { /* styles */ }
作用于其内部元素。
这种机制允许在保证样式隔离的前提下,父组件的样式仍然可以适度地影响子组件。
# 16. /deep/样式穿透
在 Vue 中,当你使用 scoped
属性时,组件的样式会被限定在组件的模板范围内。这种隔离机制是通过为每个组件生成一个独特的 data-v-*
属性来实现的。这意味着组件样式不会影响到其子组件或其他组件。然而,在某些情况下,你可能希望修改子组件或第三方组件库的默认样式,这时可以使用 /deep/
选择器来实现样式穿透。
定义:/deep/
是一个 CSS 选择器,用于在 scoped
样式中穿透组件的样式隔离,使得样式规则能够作用到子组件或深层嵌套的元素上。
用途:主要用于修改子组件样式或覆盖第三方组件库的默认样式。
使用场景
- 修改第三方组件库样式
- 当引入的第三方组件库的默认样式不符合项目需求时,可以使用
/deep/
选择器来覆盖这些样式。
- 当引入的第三方组件库的默认样式不符合项目需求时,可以使用
- 对子组件应用样式
- 在父组件中,需要对子组件中的某些元素应用特定样式时,使用
/deep/
选择器是一个解决方案。
- 在父组件中,需要对子组件中的某些元素应用特定样式时,使用
# 16-1 Vue 2 和 Vue 3 的差异
Vue 2:使用 /deep/
在 Vue 2 中,样式穿透使用 /deep/
选择器。Vue 2 默认支持 /deep/
选择器来穿透子组件的样式。
<!-- ParentComponent.vue -->
<template>
<div class="parent">
<ChildComponent />
</div>
</template>
<style scoped>
.parent {
color: blue; /* 父组件内元素默认颜色为蓝色 */
}
/* 使用 /deep/ 选择器让样式作用于子组件中的 button 元素 */
.parent /deep/ button {
background-color: red; /* 作用于子组件中的 button */
color: white;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 在 Vue 2 中,你可以直接使用
/deep/
来让父组件样式影响子组件中的button
元素。
Vue 3:使用 :deep()
在 Vue 3 中,/deep/
选择器已被弃用,官方推荐使用 :deep()
作为深度选择器。这是一个函数形式的 CSS 选择器,用于穿透 scoped
样式隔离。
<!-- ParentComponent.vue -->
<template>
<div class="parent">
<ChildComponent />
</div>
</template>
<style scoped>
.parent {
color: blue; /* 父组件内元素默认颜色为蓝色 */
}
/* 使用 :deep() 选择器让样式作用于子组件中的 button 元素 */
.parent :deep(button) {
background-color: red; /* 作用于子组件中的 button */
color: white;
}
</style>
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
- 在 Vue 3 中,使用
:deep()
代替/deep/
,并把目标选择器作为参数传递给:deep()
。
# 16-2 样式穿透机制原理
- 原理:使用
scoped
属性时,Vue 会为每个组件生成唯一的data-v-*
属性,确保组件的样式仅在该组件的范围内生效,避免样式污染。 - 样式穿透:通过使用
/deep/
或:deep()
选择器,父组件的样式可以突破scoped
的限制,作用到子组件或第三方库的元素上。使用穿透选择器时,可以访问并修改子组件或深层嵌套元素的样式。 - 效果:在浏览器中查看元素样式时,样式会应用到指定的深层元素,并通过选择器穿透
scoped
样式的作用范围。例如:- 父组件的样式:
.parent[data-v-3c83f0b7]
- 使用穿透选择器后:
.parent[data-v-3c83f0b7] /deep/ div
- 父组件的样式:
注意事项
- 性能影响:
- 过度使用
/deep/
或:deep()
选择器可能导致样式管理混乱,降低性能,因此应该谨慎使用。
- 过度使用
- 兼容性问题:
- 在 Vue 3 中,推荐使用
:deep()
替代/deep/
,因为/deep/
是非标准的,且未来的 CSS 规范可能不再支持它。
- 在 Vue 3 中,推荐使用
- 可维护性:
- 样式穿透会使得组件的样式管理变得复杂,过度依赖穿透可能会影响可维护性。应该尽量考虑使用更清晰的解决方案。
- 兼容 Vue 2 和 Vue 3:
- 如果你需要支持 Vue 2 和 Vue 3,可以通过条件判断或使用不同的 CSS 方案来保证兼容性。例如,在 Vue 2 中使用
/deep/
,在 Vue 3 中使用:deep()
。
- 如果你需要支持 Vue 2 和 Vue 3,可以通过条件判断或使用不同的 CSS 方案来保证兼容性。例如,在 Vue 2 中使用
# 16-3 最佳实践
特性 | Vue 2 | Vue 3 |
---|---|---|
样式穿透选择器 | /deep/ | :deep() |
使用方式 | /deep/ <inner-selector> | :deep(<inner-selector>) |
推荐理由 | Vue 2 支持 /deep/ ,不需要额外修改 | Vue 3 推荐使用 :deep() ,符合标准 |
兼容性问题 | Vue 2 项目中使用 /deep/ | Vue 3 项目中使用 :deep() |
- Vue 2:继续使用
/deep/
选择器来穿透子组件或第三方库样式。 - Vue 3:使用
:deep()
替代/deep/
,这是 Vue 3 中官方推荐的做法,符合 CSS 标准,保证兼容性和可维护性。
通过合理使用这些选择器,能够在不破坏组件封装的情况下,灵活地修改子组件或第三方组件的样式,避免样式冲突和管理问题。