《Vue零基础教程》(5)Vue.js组件开发
本文介绍了Vue组件的基本概念和使用方法。组件是Vue应用的构建块,采用组件化思想可以将功能模块封装复用。组件分为全局组件和局部组件两种注册方式:全局组件通过app.component()注册,在当前Vue应用中全局可用;局部组件通过components选项注册,仅限当前组件模板使用。文章还详细说明了组件的命名规范、使用注意事项以及data必须为函数的原因,并通过示例代码演示了组件的实际应用场景。
本文参考视频教程,点击这里查看
1 简介
1) 什么是组件
组件可以理解成项目的零件
项目 就是由多个 组件 构成的
举例
- 一个房子是一个Vue应用, 那么客厅/卧室/厨房/卫生间就是组件
- 一个电脑是一个Vue应用, 那么硬盘/内存/主板/显示器/键盘就是组件
组件化思想
2) 为什么需要组件
将逻辑相近的功能模块封装到一起. 方便复用
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- 文章一 --><div class="blog-item"><h3>{{title}}</h3><div>{{content}}</div></div><!-- 文章二 --><div class="blog-item"><h3>{{title}}</h3><div>{{content}}</div></div><!-- 文章三 --><div class="blog-item"><h3>{{title}}</h3><div>{{content}}</div></div></div><script>const { createApp } = Vueconst vm = createApp({data() {return {title: '文章标题',content: '文章内容',}},}).mount('#app')</script></body>
</html>
如果要使用组件需要先注册. 从注册的方式不同
可以将组件分为
- 全局组件
- 局部组件
3) 全局组件
顾名思义, 全局都可以使用的组件, 组件在当前 Vue 应用中全局可用
语法
// 可以使用 Vue 应用实例的 app.component() 方法import { createApp } from 'vue'const app = createApp({})app.component(// 注册的名字'MyComponent',// 组件的配置对象{/* ... */}
)
组件对象中的配置项和createApp
中的配置项一致
什么是当前Vue应用
每次调用createApp
会创建一个新的Vue应用. 不同的Vue应用实例有自己的空间. 不能跨应用引用组件
const app = createApp({}) // 创建一个应用实例
const app1 = createApp({}) // 创建一个新的应用实例console.log(app === app1) //false
// 在app中注册的组件, 不能在app1中使用
🤔为什么data是一个函数?
每个实例可以维护一份被返回对象的独立的拷贝:
data: function () {return {count: 0}
}
如果 Vue 没有这条规则,点击一个按钮就会影响到其它所有实例!
示例
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script>
<body><div id="app"><counter></counter><counter></counter><counter></counter></div><script>const {createApp} = Vueconst app = createApp({})// 定义组件app.component('counter', {template: '<button @click="count++">点击了{{count}}次</button>',data() {return {count: 0}}})app.mount('#app')</script>
</body>
</html>
引用
在模板中, 通过组件名称引用组件
<blog-item></blog-item>
注意事项
- 命名原则
- 闭合方式
- 引用位置
<!DOCTYPE html>
<html lang="en">
<head><meta charset="UTF-8"><meta http-equiv="X-UA-Compatible" content="IE=edge"><meta name="viewport" content="width=device-width, initial-scale=1.0"><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script>
<body><div id="app"><table><my-button></my-button></table></div><script>const {createApp} = Vueconst app = createApp({})// 定义组件// 一. 组件名称// 1. 组件名不能是HTML原生的标签名// 2. 组件名尽量使用多个单词, 多个单词之间使用-连接(kebab-case)// 3. 组件名尽量使用PascalCase(驼峰法)// 二. 组件引用// 1. 在不使用构建工具的情况下, 由于HTML不区分大小写, 所以在模板中不能使用PascalCase来引用组件// 2. 组件使用双标签形式// 3. 组件不要在ul.ol,table,select内部直接使用app.component('MyButton', {template: '<button @click="count++">点击了{{count}}次</button>',data() {return {count: 0}}})app.mount('#app')</script>
</body>
</html>
4) 局部组件
语法
使用components
选项
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><!-- 根组件解析的模板 --><div id="app"><!-- 局部组件只能在当前组件的模板中引用 --><first-com></first-com><second-com></second-com></div><script>const { createApp } = Vue// 参数{}: 根组件配置对象const app = createApp({// 定义根组件下的子组件components: {// 组件名: 组件配置对象FirstCom: {template: `<div>FirstCom</div>`,},SecondCom: {template: `<div>SecondCom</div>`,},},})app.mount('#app')</script></body>
</html>
如何理解局部
局部注册的组件在**后代组件中不可用****, 只能在当前组件的模板中引用**
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><!-- 根组件解析的模板 --><div id="app"><!-- 局部组件只能在当前组件的模板中引用 --><first-com></first-com><second-com></second-com></div><script>const { createApp } = Vue// 参数{}: 根组件配置对象// 在当前组件的配置项中定义的组件只能在当前组件对应的模板中使用const app = createApp({// 定义根组件下的子组件components: {// 组件名: 组件配置对象FirstCom: {template: `<div>不能在FirstCom组件的模板中引用父组件配置对象中定义的组件<second-com></second-com></div>`,},SecondCom: {template: `<div>SecondCom</div>`,},},})// const app = createApp({})// app// .component('FirstCom', {// template: `<div>// FirstCom// -- <second-com></second-com>// </div>`,// })// .component('SecondCom', {// template: '<div>SecondCom</div>',// })app.mount('#app')</script></body>
</html>
2 父子组件间通信(重点掌握)
1) 为什么需要通信
场景
父组件Root
, 子组件为BlogItem
每一篇blog的标题都不同. 子组件只能决定结构和样式. 具体的数据要变化, 需要父组件通知
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- 1. 在父组件的模板中, 通过自定义属性传值 --><blog-item title="Vue深入浅出"></blog-item><blog-item :title="msg"></blog-item><blog-item title="Vue的最佳实践"></blog-item></div><script>const { createApp } = Vue// 父组件: 确定数据const vm = createApp({data() {return {msg: 'Vue的设计与实现',}},components: {// 子组件定义: 确定结构和样式BlogItem: {// 在子组件的配置项中, 通过props接收props: ['title'],template: `<div><h3>{{title}}</h3><p>内容</p></div>`,},},}).mount('#app')</script></body>
</html>
2) 父传子
通过子组件的自定义属性
- 在父组件模板中, 设置自定义属性
- 在子组件选项中, 通过
props
接收
示例
<!-- 在父组件的模板中, 设置自定义属性 -->
<blog-item title="Vue深入浅出"></blog-item>
<!-- 在父组件的模板中, 绑定自定义属性 -->
<blog-item :title="msg"></blog-item>
{// 在子组件的配置项中, 通过props接收props: ['title'],template: `<div><h3>{{title}}</h3><p>内容</p></div>`,
},
3) 子传父
借助子组件的中自定义事件
- 在子组件中, 触发自定义事件
- 在父组件中, 监听自定义事件
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- 1. 在父组件的模板中, 通过自定义属性传值 --><blog-item title="Vue深入浅出"></blog-item><blog-item :title="msg"></blog-item><!-- 2. 在父组件的模板, 监听自定义事件 --><blog-item v-if="show" title="Vue的最佳实践" @close="handleClose"></blog-item></div><script>const { createApp } = Vue// 父组件: 确定数据const vm = createApp({data() {return {msg: 'Vue的设计与实现',show: true,}},methods: {handleClose(i) {this.show = falseconsole.log(i)},},components: {// 子组件定义: 确定结构和样式BlogItem: {// 在子组件的配置项中, 通过props接收props: ['title'],template: `<div><h3>{{title}}</h3><p>内容</p>// 1. 在子组件中触发一个自定义事件<button @click="$emit('close', 1)">关闭</button></div>`,},},}).mount('#app')</script></body>
</html>
3 深入组件
1) props(重点掌握)
语法
props选项除了我们在最开始使用的数组语法
外, 还支持对象语法
在实际开发中, 绝大多数情况使用对象语法
对象语法
- 支持类型判断
- 支持默认值
- 是否必传
- 自定义校验器
示例
export default {props: {// 基础类型检查//(给出 `null` 和 `undefined` 值则会跳过任何类型检查)propA: Number,// 多种可能的类型propB: [String, Number],// 必传,且为 String 类型propC: {type: String,required: true},// Number 类型的默认值propD: {type: Number,default: 100},// 对象类型的默认值propE: {type: Object,// 对象或者数组应当用工厂函数返回。// 工厂函数会收到组件所接收的原始 props// 作为参数default(rawProps) {return { message: 'hello' }}},// 自定义类型校验函数propF: {validator(value) {// The value must match one of these stringsreturn ['success', 'warning', 'danger'].includes(value)}},// 函数类型的默认值propG: {type: Function,// 不像对象或数组的默认,这不是一个工厂函数。这会是一个用来作为默认值的函数default() {return 'Default function'}}}
}
支持的type类型
命名
如果props
属性由多个单词组成. 更推荐的方式
- 在js中, 使用
camelCase
(小驼峰法)命名 - 在html中, 使用
kebab-case
命名
单向数据流
在子组件中不能直接修改props的值
2) 事件(理解)
子组件可以通过$emit
向父组件提交一个自定义事件, 并且可以携带参数
和原生 DOM 事件不一样,组件触发的事件没有冒泡机制。只能监听直接子组件触发的事件。平级组件或是跨越多层嵌套的组件间通信,应使用一个外部的事件总线,或是使用一个全局状态管理方案。
命名
基本原则
- 在JS中, 使用
camelCase
- 在HTML中, 使用
kebab-case
emits选项
组件要触发的事件可以显式地通过 emits 选项来声明
export default {emits: ['inFocus', 'submit']
}
export default {emits: {submit(payload) {// 通过返回值为 `true` 还是为 `false` 来判断// 验证是否通过}}
}
3) 透传属性(了解)
什么是透传属性
在父组件模板中设置, 但是没有被子组件接收的属性
常见的透传属性
最常见的例子就是 class
、style
和 id
当子组件只有一个根节点,透传属性会自动被添加到根元素上
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script><style>.red {color: red;}</style></head><body><div id="app"><my-button title="子组件" abc="透传属性" class="red"></my-button></div><script>const { createApp } = Vueconst vm = createApp({components: {MyButton: {props: {title: {type: String,},},template: `<button>{{title}}</button>`,},},}).mount('#app')</script></body>
</html>
如果需要自定义挂载时机, 不想默认挂载到根节点, 可以
- 禁用继承
- 手动绑定
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script><style>.red {color: red;}</style></head><body><div id="app"><my-button title="子组件" abc="透传属性" class="red"></my-button></div><script>const { createApp } = Vueconst vm = createApp({components: {MyButton: {inheritAttrs: false,props: {title: {type: String,},},template: `<div><button v-bind="$attrs">{{title}}</button></div>`,},},}).mount('#app')</script></body>
</html>
当子组件存在多个根节点, 需要显式的指定, 否则会报错
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script><style>.red {color: red;}</style></head><body><div id="app"><my-button title="子组件" abc="透传属性" class="red"></my-button></div><script>const { createApp } = Vueconst vm = createApp({components: {MyButton: {props: {title: {type: String,},},template: `<header></header><main v-bind="$attrs"></main><footer></footer>`,},},}).mount('#app')</script></body>
</html>
4) 插槽(理解)
插槽的基本概念
什么是插槽
插槽(slot)可以理解为预留了一个可替换的地方
游戏卡是可以插拔的, 插游戏卡的地方就是一个插槽
思考
游戏卡插槽有什么作用?
再比如, USB 接口也可以看成一个插槽
. 可以插入 U 盘, 硬盘, 鼠标, 键盘…
还有, CPU 槽, 内存槽. 他们的存在有什么共同点??
为什么需要插槽
通过上面的例子, 我们可以看出
- 通过插不同的游戏卡, 可以玩不同的游戏
- 通过插不同的外设, 可以扩展电脑的功能
- 通过插不同型号的 CPU(i3/i5/i7/i9), 可以更换 CPU
插槽最主要的作用是提供扩展性
.
在 Vue 开发中, slot 主要应用在组件开发中, 通过在组件中预留 slot, 实现不同的功能
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- my-com只是一占位符, 子组件挂载的过程: 编译后替换整个占位符 --><my-com>不管在这里写什么内容, 都不会被渲染</my-com></div><script>const { createApp } = Vueconst vm = createApp({components: {MyCom: {template: `<div>子组件</div>`,},},}).mount('#app')</script></body>
</html>
子组件的模板编译后, 会替换<my-com>
所在的地方
不管在<my-com>
中添加任何内容, 都不起作用~ 为什么? 因为会被替换
这样, 子组件的可扩展性就很不好. 如果希望子组件中的内容可以替换怎么办??
在子组件中预留一个插槽
, 通过给子组件传递不同的内容来改变子组件
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- my-com只是一占位符, 子组件挂载的过程: 编译后替换整个占位符 --><!-- <my-com>不管在这里写什么内容, 都不会被渲染</my-com> --><my-com>传给插槽的内容</my-com></div><script>const { createApp } = Vueconst vm = createApp({components: {MyCom: {// 1. 在子组件中, 预留slot插槽template: `<div><slot></slot></div>`,},},}).mount('#app')</script></body>
</html>
具名插槽
作用
如果需要同时使用多个插槽, 就需要给插槽取名字.
就好比: 主板上同时有 CPU 槽和内存槽, 如何区分这两个插槽, 不至于把内存插到 CPU 中
当然, 现实中肯定不会, 但是程序中就需要使用名字区分开
示例
- 在子组件中, 定义具名插槽
- 在引用子组件时, 通过
slot属性
指定要替换的插槽
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><computer><div>这是一个电脑</div><!-- v-slot:插槽的名字 --><template v-slot:cpu><div>这里放CPU的</div></template><template v-slot:memery><div>这里放内存的</div></template>这里的内容会被插入到默认插槽中</computer></div><script>const { createApp } = Vueconst vm = createApp({components: {computer: {// 1. 在子组件中, 预留slot插槽template: `<div><slot name="default"></slot><slot name="cpu"></slot><slot name="memery"></slot></div>`,},},}).mount('#app')</script></body>
</html>
没有指定template的内容会全部放到<slot>
中, 也就是默认插槽<slot name="default">
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><computer><div>这是一个电脑</div><!-- v-slot:插槽的名字 --><template v-slot:cpu><div>这里放CPU的</div></template><template v-slot:memery><div>这里放内存的</div></template>这里的内容会被插入到默认插槽中</computer></div><script>const { createApp } = Vueconst vm = createApp({components: {computer: {// 1. 在子组件中, 预留slot插槽template: `<div><slot name="default"></slot><slot name="cpu"></slot><slot name="memery"></slot></div>`,},},}).mount('#app')</script></body>
</html>
作用域插槽
编译作用域
在 Vue 编译的过程中, 如果父子组件中定义的相同的
状态
, 会不会冲突呢?
如果不会冲突, 具体访问的是哪个
状态
呢<
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><!-- 父组件模板 --><div id="app"><son v-show="isShow"></son></div><!-- 子组件的模板 --><template id="tmp"><div><h3>我是子组件</h3><button v-show="isShow">子组件按钮</button></div></template><script>const { createApp } = Vueconst vm = createApp({data() {return {isShow: false,}},components: {son: {template: '#tmp',data() {return {isShow: true,}},},},}).mount('#app')</script></body>
</html>
父组件和子组件中都存在isShow
.
- 如果在父模板中使用 isShow, 访问的是父组件
data
中的值 - 如果在子模板中使用 isShow, 访问的是子组件
data
中的值
通过上述示例, 我们可以发现, 在父组件中是**不能**直接访问子组件中的状态的.
为什么需要作用域插槽
为了解决上述问题, 引入了作用域插槽的概念, 其核心是在父模板中访问子组件的数据
示例
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><!-- 父组件模板 --><div id="app"><son><!-- 通过v-slot指令定义默认插槽的名字, 可以随便起. 表示一个slot对象 {test: 'hello', row: {name: 'xiaoming'}}--><template v-slot:default="slotProps">{{slotProps.test}} -- {{slotProps.row.name}}</template><!-- cpuSlot: {type: 'I7 12700K'} --><!-- v-slot的简写: # --><template #cpu="cpuSlot"><div>{{cpuSlot.type}}</div></template></son></div><!-- 子组件模板 --><template id="temp"><h3>子组件</h3><!-- 1. 在slot上绑定一个自定义属性 --><slot :test="msg" :row="user"></slot><slot name="cpu" :type="cpu"></slot></template><script>const { createApp } = VuecreateApp({components: {son: {template: '#temp',data() {return {msg: 'hello',user: {name: 'xiaoming',},cpu: 'I7 12700K',}},},},}).mount('#app')</script></body>
</html>
其中, v-slot
指令可以使用#
简写
<template #cpu="cpuSlot"><div>{{cpuSlot.type}}</div>
</template>
5) 动态组件
场景
根据type
类型不同, 渲染不同的组件
- 当type为
image
时, 渲染图片组件 - 当type当
video
时, 渲染视频组件
传统的做法
使用v-if
判断
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- <div v-if="type == 'image'"><my-image></my-image></div><div v-else-if="type == 'video'"><my-video></my-video></div> --><!-- 动态组件: 根据不同的状态, 渲染不同的组件 --><!-- 使用v-if指令实现 --><my-image v-if="type=='image'"></my-image><my-video v-else-if="type=='video'"></my-video></div><script>const { createApp } = Vueconst vm = createApp({data() {return {type: 'image',name: 'MyImage',}},components: {MyImage: {template: `<img src=""/>`,},MyVideo: {template: `<video></video>`,},},}).mount('#app')</script></body>
</html>
动态组件
<!DOCTYPE html>
<html lang="en"><head><meta charset="UTF-8" /><meta http-equiv="X-UA-Compatible" content="IE=edge" /><meta name="viewport" content="width=device-width, initial-scale=1.0" /><title>Document</title><script src="../node_modules/vue/dist/vue.global.js"></script></head><body><div id="app"><!-- 动态组件: 根据不同的状态, 渲染不同的组件 --><!-- 使用component内置组件is绑定组件名--><component :is="name"></component></div><script>const { createApp } = Vueconst vm = createApp({data() {return {type: 'image',name: 'MyImage',}},components: {MyImage: {template: `<img src=""/>`,},MyVideo: {template: `<video></video>`,},},}).mount('#app')</script></body>
</html>
动态组件
渲染的组件是动态生成, 根据不同的状态渲染不同的组件