前言
开发过程中我们会经常遇到一些复杂的页面,而这些页面大部分由一个个小部分组合起来的,而且不同页面中可能有些部分是一样的,所以我们通常会将这些部分封装成组件。在Vue中,我们可以使用components组件(模板)来实现。
实现一个组件
一个组件其实就是一个vue文件,简单示例(header.vue)如下:
代码语言:javascript复制<script setup></script>
<template>
<div class="header"></div>
</template>
<style scoped lang="less">
.header {
position: absolute;
width: 100%;
height: 80px;
background: linear-gradient(
180deg,
rgba(0, 0, 0, 0.6) 0%,
rgba(0, 0, 0, 0) 100%
);
}
</style>
注册使用
基于script setup可以自动注册组件,只需要import即可使用,如下:
代码语言:javascript复制<script setup>
import Header from "./Header.vue";
</script>
<template>
<div class="container">
<Header></Header>
</div>
</template>
<style scoped lang="less">
.container {
width: 100%;
height: 100%;
}
</style>
数据
可以通过Prop向组件传递数据,先在组件中定义,如下:
代码语言:javascript复制<script setup>
const props = defineProps({
title: String,
userInfo: Object
});
function doSomething(){
if(props.userInfo.isVip){
...
}
}
</script>
<template>
<div class="header">{{props.title}}</div>
</template>
<style scoped lang="less">
...
</style>
这里定义了一个title属性,是一个字符串;一个userInfo属性,是一个对象,然后在组件中就可以通过props.xxx
来使用这些属性。
那么如何将数据传递给这些属性呢,直接通过v-bind绑定数据即可,如下:
代码语言:javascript复制<script setup>
import Header from "./Header.vue";
let userInfo;
let title;
</script>
<template>
<div class="container">
<Header :title="title" :userInfo="userInfo"></Header>
</div>
</template>
<style scoped lang="less">
...
</style>
这里使用的是v-bind的缩写。v-bind绑定后面双引号中是表达式,所以如果类型是:
- 数值:
:count="3"
- 布尔值:
:isVip="true"
- 数组:
:array="[1,2,3]"
- 对象:
:info="{name:'名字',isVip:true}"
其他就不一一列举了。
Props是支持类型如下:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol。
Props支持类型检查,同时支持默认值,如下:
代码语言:javascript复制 props: {
// 基础的类型检查 (`null` 和 `undefined` 值会通过任何类型验证)
propA: Number,
// 多个可能的类型
propB: [String, Number],
// 必填的字符串
propC: {
type: String,
required: true
},
// 带有默认值的数字
propD: {
type: Number,
default: 100
},
// 带有默认值的对象
propE: {
type: Object,
// 对象或数组的默认值必须从一个工厂函数返回
default() {
return { message: 'hello' }
}
},
// 具有默认值的函数
propG: {
type: Function,
// 与对象或数组的默认值不同,这不是一个工厂函数——这是一个用作默认值的函数
default() {
return 'Default function'
}
}
}
Props是单向数据流,这样可以防止子组件意外变更父组件的状态,每当父组件发生变更,子组件所有Props都会刷新到最新值。所以子组件中不能更改Props的属性,否则会在控制台警告。
事件
数据传递清楚了,那么如何响应组件的一些自定义事件呢?通过emits来定义事件,如下:
代码语言:javascript复制<script setup>
const emit = defineEmits(["onSelected"]);
function selectItem(item){
emit("onSelected", item);
}
</script>
<template>
<div class="header" >
...
</div>
</template>
<style scoped lang="less">
...
</style>
这里定义了一个onSelected事件,当组件中触发selectItem函数的时候就会执行这个事件。
在父组件中则通过v-on来绑定事件,如下:
代码语言:javascript复制<script setup>
import Header from "./Header.vue";
function headerSelected(item){
}
</script>
<template>
<div class="container">
<Header @onSelected="headerSelected"></Header>
</div>
</template>
<style scoped lang="less">
...
</style>
这里同样是v-on的缩写形式,这样就绑定了事件。事件同样可以验证,这里就不细说了。
v-model
v-model是双向数据绑定,默认情况下,组件上的 v-model 使用 modelValue 作为 prop 和 update:modelValue 作为事件。比如有一个title属性:
代码语言:javascript复制<my-component v-model:title="bookTitle"></my-component>
那么在子组件中就可以这样做:
代码语言:javascript复制<script setup>
const props = defineProps({
title: String
});
const emit = defineEmits(["update:title"]);
</script>
<template>
<div class="header" >
...
</div>
</template>
<style scoped lang="less">
...
</style>
这样子组件中可以通过update:title
来同步title数据。
插槽
如果子组件中部分区域是不定的,需要父组件来实现,那么怎么办?这就需要用到插槽slot,插槽使用很简单,如下:
代码语言:javascript复制<script setup>
...
</script>
<template>
<div class="header" >
<slot>default</slot>
</div>
</template>
<style scoped lang="less">
...
</style>
这里定义了一个插槽,并且指定了一个默认内容,在父组件中:
代码语言:javascript复制<script setup>
import Header from "./Header.vue";
</script>
<template>
<div class="container">
<Header>newtitle</Header>
</div>
</template>
<style scoped lang="less">
...
</style>
这样插槽就被“newtitle”这个字符串替代,如果这里没有任何内容<Header></Header>
则显示默认内容,即"default"。
插槽中不仅是字符串,可以是html或其他组件,比如:
代码语言:javascript复制<Header>
<button>submit</botton>
</Header>
当然默认内容也可以是html。
具名插槽
一个组件里可以有多个插槽,比如左边栏右边栏等,这样就需要具名插槽来区分,如下:
代码语言:javascript复制<script setup>
...
</script>
<template>
<div class="header" >
<div name="leftbar" >
<slot>left</slot>
</div>
<div name="rightbar" >
<slot>right</slot>
</div>
</div>
</template>
<style scoped lang="less">
...
</style>
使用的时候需要用v-slot指令,并且一定是<template>
元素,因为v-slot只能添加在<template>
上,如下:
<script setup>
import Header from "./Header.vue";
</script>
<template>
<div class="container">
<Header>
<template v-slot:leftbar >
<button>left</button>
</template>
<template v-slot:rightbar >
<button>right</button>
</template>
</Header>
</div>
</template>
<style scoped lang="less">
...
</style>
插槽名也可以是动态的<template v-slot:[dynamicSlotName] >
。v-slot也可以缩写成“#”,如<template #leftbar >
。
调用子组件方法
上面事件章节说的是父组件响应子组件的事件,也就是说是子组件调用父组件的方法。那么父组件如何调用子组件的方法?
Expose
首先子组件的方法需要暴露出去,如下:
代码语言:javascript复制<script setup>
defineExpose({
onEvent
});
function onEvent(event) {
console.log(`header: ${event}`);
}
</script>
<template>
<div class="header" >
...
</div>
</template>
<style scoped lang="less">
...
</style>
然后在父组件中为子组件添加ref属性,然后通过ref()函数获取对象执行方法即可,如下:
代码语言:javascript复制<script setup>
import Header from "./Header.vue";
const headerRef = ref();
function headerEvent(event){
headerRef.value.onEvent(event);
}
</script>
<template>
<div class="container">
<Header ref="headerRef">
...
</Header>
</div>
</template>
<style scoped lang="less">
...
</style>
上面header的ref属性是“headerRef”,所以变量名也保持一致,即const headerRef = ref();
,然后就可以通过headerRef.value
执行暴露出来的方法了。
如果有多个子组件,都设置了ref属性,则定义多个变量即可,如下:
代码语言:javascript复制const headerRef = ref();
const footerRef = ref();
EventBus
更简单的方法是使用EventBus即可完成组件间通信,使用也非常简单,参考官方文档即可 https://www.npmjs.com/package/events
我们可以简单封装一下以便使用,如下:
代码语言:javascript复制import EventEmitter from "events";
const eventEmitter = new EventEmitter();
export const WsEvent = {
emit: data => {
eventEmitter.emit("wsevent", data);
},
on: callback => {
eventEmitter.on("wsevent", callback);
}
};