简言
在以前vue3组件学习使用和利用递归组件实现简单的树组件中,分别讲了组件的基本使用和一部分高级用法(provide/inject)。这篇文章来将组件其他的使用方法。
话不多说,不玩虚的,let’s go! ——ZSK666
正文
主要讲组件插槽(slot)、组件透传属性、异步组件和动态组件等其他高级用法。
组件透传Attributes
组件透传中的attributes指的是传递给一个组件,却没有被该组件声明为 props 或 emits 的 attribute 或者 v-on 事件监听器,将会自动传递给组件根元素。最常见的例子就是 class、style 和 id。啥意思呢?意思就是只要组件不认识或者没声明的属性或事件,通通自动在组件根元素上发挥作用!
这是一个组件:
<!-- <MyButton> 的模板 -->
<button>click me</button>
我们使用时,定义一个class:
<MyButton class="large" />
最后渲染出的 DOM 结果是:
<button class="large">click me</button>
由于组件中没有将class声明为props的属性,所以就当普通属性传递到了组件根元素上。
组件透传属性有以下特点:
- 透传的class、style和事件,会和组件中根元素的class、style和事件发生合并;两者的属性都会起作用。
- 可以深度传递,组件的根元素如果还是组件,透传属性中不符合根元素组件props和emit声明的属性还会透传到根元素组件中。(一般不会这样写,代码写的要自己和他人看的懂)
- 使用v-bind可以传递一个透传属性的对象。(类似将透传属性写在一一起,然后使用v-bind一快传递到组件中)
- 这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。在JavaScript中可以使用$attrs 这个实例属性(this.$attrs)来访问组件的所有透传 attribute。
上面的是组件只有一个根组件的情况,现在vue3不是组件可以多个根元素了嘛,那传递过来的透传属性咋办呢?很简单,为了不让组件内的根元素互相争呢,改了,改成不是自动的了,要显式指定哪个元素来接收透传属性才可以,不然会发生警告!显式指定很简单,你看哪个元素需要,在元素标签内
使用v-bind="$attrs"就行。
和单根节点组件有所不同,有着多个根节点的组件没有自动 attribute 透传行为。如果 $attrs 没有被显式绑定,将会抛出一个运行时警告。
<CustomLayout id="custom-layout" @click="changeValue" />
如果 <CustomLayout> 有下面这样的多根节点模板,由于 Vue 不知道要将 attribute 透传到哪里,所以会抛出一个警告。
<header>...</header>
<main>...</main>
<footer>...</footer>
如果 $attrs 被显式绑定,则不会有警告:
<header>...</header>
<main v-bind="$attrs">...</main>
<footer>...</footer>
下面是我写的一个小案例。
组件:
<!--
* @Date: 2022-11-14 14:36:16
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-14 14:38:56
* @FilePath: \basic-demo\src\components\common\AttributeBox.vue
* @Label: Do not edit
-->
<template>
<div class="attribute__box1" v-bind="$attrs">
<div>透传属性:</div>
<div>{{ $attrs }}</div>
</div>
<div class="attribute__box2">
<div>props的属性arr:</div>
<div>{{ arr }}</div>
</div>
<div class="attribute__box3"></div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onBeforeMount, onMounted } from "vue";
defineProps({
arr: {
// props的属性
type: Array,
default: () => [],
},
});
</script>
<style lang="scss" scoped></style>
使用:
<!--
* @Date: 2022-10-27 15:46:26
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-14 14:41:42
* @FilePath: \basic-demo\src\pages\index.vue
* @Label: Do not edit
-->
<template>
<div class="container">
<h1>hello,World!</h1>
<!-- 透传属性 -->
<AttributeBoxVue
class="woshi"
v-bind="attributes"
:arr="['1', '2', '3']"
></AttributeBoxVue>
</div>
</template>
<script lang="ts" setup>
import CalculateVue from "@/components/common/Calculate.vue";
import TreeVue from "@/components/Tree/index.vue";
import AttributeBoxVue from "@/components/common/AttributeBox.vue";
import { reactive, toRefs, ref, onBeforeMount, onMounted } from "vue";
// 透传属性
const attributes = {
style: "backgroundColor:#eaeaea;",
class: "bind",
hhh: "lalala",
};
</script>
<style lang="scss" scoped></style>
效果:
异步组件(动态加载)
异步组件就是动态加载组件,在页面需要这个组件的时候再加载这个组件。
vue2的时候是利用es模块的动态导入,然后根据引入组件支持函数这个特性,实现动态加载。
// 引入
const dongtai = ()=> import(...)
// 然后dongtai函数在组件注册时使用
vue3有点不一样。Vue3 提供了 defineAsyncComponent 方法来实现此功能。defineAsyncComponent 方法接收一个返回 Promise 的加载函数。这个 Promise 的 resolve 回调方法应该在从服务器获得组件定义时调用。你也可以调用 reject(reason) 表明加载失败。:
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() => {
return new Promise((resolve, reject) => {
// ...从服务器获取组件
resolve(/* 获取到的组件 */)
})
})
// ... 像使用其他一般组件一样使用 `AsyncComp`
和es模块动态导入搭配使用:
最后得到的 AsyncComp 是一个外层包装过的组件,仅在页面需要它渲染时才会调用加载内部实际组件的函数。它会将接收到的 props 和插槽传给内部组件,所以你可以使用这个异步的包装组件无缝地替换原始组件,同时实现延迟加载。
import { defineAsyncComponent } from 'vue'
const AsyncComp = defineAsyncComponent(() =>
import('./components/MyComponent.vue')
)
高级配置:
const AsyncComp = defineAsyncComponent({
// 加载函数
loader: () => import('./Foo.vue'),
// 加载异步组件时使用的组件
loadingComponent: LoadingComponent,
// 展示加载组件前的延迟时间,默认为 200ms
delay: 200,
// 加载失败后展示的组件
errorComponent: ErrorComponent,
// 如果提供了一个 timeout 时间限制,并超时了
// 也会显示这里配置的报错组件,默认值是:Infinity
timeout: 3000
})
如果提供了一个加载组件,它将在内部组件加载时先行显示。在加载组件显示之前有一个默认的 200ms 延迟——这是因为在网络状况较好时,加载完成得很快,加载组件和最终组件之间的替换太快可能产生闪烁,反而影响用户感受。
如果提供了一个报错组件,则它会在加载器函数返回的 Promise 抛错时被渲染。你还可以指定一个超时时间,在请求耗时超过指定时间时也会渲染报错组件。
插槽(slot)
在某些场景中,我们可能想要为子组件传递一些模板片段,让子组件在它们的组件中渲染这些片段。
上面这句话意思就是JavaScript的内容可以利用props传递,html的内容我也想传递给组件,赶快想个能接收html内容的法子;哎,插槽(slot)就是干这事的, 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。
举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:
<FancyButton>
Click me! <!-- 插槽内容 -->
</FancyButton>
而 <FancyButton> 的模板是这样的:
<button class="fancy-btn">
<slot></slot> <!-- 插槽出口 -->
</button>
最终渲染出的 DOM 是这样:
<button class="fancy-btn">Click me!</button>
slot元素内可以填入默认内容,当使用时不写插槽内容后,会渲染默认内容。
<button class="fancy-btn">
<slot>我是默认内容</slot> <!-- 插槽出口 -->
</button>
使用:
<FancyButton>
</FancyButton>
渲染结果:
<button class="fancy-btn">我是默认内容</button>
组件内可以使用多个slot元素来写多个插槽,每个插槽需要写不同的name属性来标识,默认插槽的name等于default。
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
使用时,需要使用template元素来包裹不同的插槽内容,同时使用v-slot:name(插槽定义名)来使用具名插槽。
<BaseLayout>
<template v-slot:header>
<!-- header 插槽的内容放这里 -->
</template>
</BaseLayout>
v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。
<BaseLayout>
<template #header>
<h1>Here might be a page title</h1>
</template>
<template #default>
<p>A paragraph for the main content.</p>
<p>And another one.</p>
</template>
<template #footer>
<p>Here's some contact info</p>
</template>
</BaseLayout>
这里面有个重要的内容需要说明,Vue 模板中的表达式只能访问其定义时所处的作用域,这和 JavaScript 的词法作用域规则是一致的。意思是你在哪写的就只能用哪的数据,你在父组件写的内容只能访问父组件数据,在组件内写的只能访问组件内的数据。
组件内写的插槽默认内容可以使用props访问父组件的数据。
父组件写的插槽内容怎么访问组件内的数据呢?
下面是使用方法:
组件可以像对组件传递 props 那样,向一个插槽的出口上传递 attributes(使用具名插槽时,name不会传递):
<!-- <MyComponent> 的模板 -->
<div>
<slot :text="greetingMessage" :count="1"></slot>
</div>
父组件使用插槽内容:
<!-- 这里使用了解构 -->
<MyComponent v-slot="{ text, count }">
{{ text }} {{ count }}
</MyComponent>
具名作用域插槽的工作方式也是类似的,插槽 props 可以作为 v-slot 指令的值被访问到:v-slot:name=“slotProps”。当使用缩写时是这样:
<MyComponent>
<template #header="headerProps">
{{ headerProps }}
</template>
<template #default="defaultProps">
{{ defaultProps }}
</template>
<template #footer="footerProps">
{{ footerProps }}
</template>
</MyComponent>
当使用具名插槽后,组件上就不能使用v-slot来接收默认插槽传递的内容,否则会报错。
下面是小示例:
组件:
<!--
* @Date: 2022-11-14 15:35:46
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-14 15:44:27
* @FilePath: \basic-demo\src\components\common\SlotBox.vue
* @Label: Do not edit
-->
<template>
<div class="slot__box">
<h1>插槽(slot)</h1>
<div class="main">
<slot text="默认插槽的text">
<div>我是插槽默认内容div</div>
<p>我是插槽默认内容p</p>
</slot>
</div>
<div class="footer">
<slot name="footer">
<div>我是插槽footer的div:title --{{ title }}</div>
</slot>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onBeforeMount, onMounted } from "vue";
defineProps({
title: {
type: String,
default: "",
},
});
</script>
<style lang="scss" scoped></style>
使用:
<!--
* @Date: 2022-10-27 15:46:26
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-14 15:40:44
* @FilePath: \basic-demo\src\pages\index.vue
* @Label: Do not edit
-->
<template>
<div class="container">
<h1>hello,World!</h1>
<!-- 插槽 -->
<SlotBoxVue title="我是父组件传递的标题">
<template #default="{ text }"> {{ text }}</template>
<template #footer></template>
</SlotBoxVue>
</div>
</template>
<script lang="ts" setup>
import SlotBoxVue from "@/components/common/SlotBox.vue";
</script>
<style lang="scss" scoped></style>
效果:
动态组件
通过 Vue 的 元素和特殊的 is attribute 可以实现动态加载显示某个组件。
<!-- currentTab 改变时组件也改变 -->
<component :is="currentTab"></component>
在上面的例子中,被传给 :is 的值可以是以下几种:
- 被注册的组件名
- 导入的组件对象
你也可以使用 is attribute 来创建一般的 HTML 元素。
官方举得例子是第一种,传递被注册的组件名,来实现切换tab效果。
我来用第二种写一个简单的示例:
组件:
<!--
* @Date: 2022-11-14 16:01:46
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-15 10:07:52
* @FilePath: \basic-demo\src\components\common\DynamicComponent.vue
* @Label: Do not edit
-->
<template>
<div class="dynamic__component">
<h1>动态组件</h1>
<div v-for="item of componentsArr">
<component :is="item.name" v-bind="item.attrs" v-on="item.listenEvents">
<template v-if="typeof item.content === 'string'">
{{ item.content }}
</template>
</component>
<component
v-if="typeof item.content !== 'string' && item.content"
:is="item.content.name"
v-bind="item.content.attrs"
>{{ item.content.content }}</component
>
</div>
<div>
<div>form值</div>
<div v-for="(item, key) of form">{{ key }} -- {{ item }}</div>
</div>
</div>
</template>
<script lang="ts" setup>
import { reactive, toRefs, onBeforeMount, onMounted, ref } from "vue";
interface component {
name: string; // 组件名
attrs: object; //组件属性
key: string;
listenEvents?: object; // 监听事件名
content?: any; // 组件内容
}
interface stringKey {
[key: string]: any;
}
interface obj extends stringKey {
inputValue: string;
checkedNames: Array<string>;
}
interface Props {
componentsArr: Array<component>;
form: obj;
}
defineProps<Props>();
</script>
<style lang="scss" scoped></style>
使用:
<!--
* @Date: 2022-10-27 15:46:26
* @LastEditors: zhangsk
* @LastEditTime: 2022-11-15 10:11:35
* @FilePath: \basic-demo\src\pages\index.vue
* @Label: Do not edit
-->
<template>
<div class="container">
<h1>hello,World!</h1>
<!-- 动态组件 -->
<DynamicComponentVue
:components-arr="componentsArr"
:form="form"
></DynamicComponentVue>
</div>
</template>
<script lang="ts" setup>
import DynamicComponentVue from "@/components/common/DynamicComponent.vue";
import { reactive, toRefs, ref, onBeforeMount, onMounted, watch } from "vue";
// 动态组件
interface component {
name: string; // 组件名
attrs: object; //组件属性
key: string;
listenEvents?: object; // 监听事件名
content?: any; // 组件内容
}
const value = ref("1");
interface stringKey {
[key: string]: any;
}
interface obj extends stringKey {
inputValue: string;
checkedNames: Array<string>;
}
const form = reactive({
inputValue: "",
checkedNames: [],
}) as any;
const componentsArr: Array<component> = [
{
name: "input",
key: "inputValue",
attrs: {
type: "text",
placeholder: "请输入值",
"v-model": form.inputValue,
},
listenEvents: {
input: (e: any) => {
value.value = e.target.value;
form.inputValue = e.target.value;
},
},
},
{
name: "input",
key: "checkedNames",
attrs: {
type: "checkbox",
value: "one",
id: "one",
"v-model": form.checkedNames,
},
listenEvents: {
change: (e: any) => {
CheckboxChange(e.target.value);
},
},
content: {
name: "label",
attrs: {
for: "one",
},
content: "one",
},
},
{
name: "input",
key: "checkedNames",
attrs: {
type: "checkbox",
value: "two",
id: "two",
"v-model": form.checkedNames,
},
listenEvents: {
change: (e: any) => {
CheckboxChange(e.target.value);
},
},
content: {
name: "label",
attrs: {
for: "two",
},
content: "two",
},
},
{
name: "input",
key: "checkedNames",
attrs: {
type: "checkbox",
value: "three",
id: "three",
"v-model": form.checkedNames,
},
listenEvents: {
change: (e: any) => {
CheckboxChange(e.target.value);
},
},
content: {
name: "label",
attrs: {
for: "three",
},
content: "three",
},
},
];
watch(
() => value.value,
() => {
console.log(value.value, "value改变");
}
);
const CheckboxChange = (v: string) => {
if (form.checkedNames.includes(v)) {
form.checkedNames = form.checkedNames.filter((v2: string) => v2 !== v);
} else {
form.checkedNames.push(v);
}
};
</script>
<style lang="scss" scoped></style>
效果:
这样写有点麻烦,vue2时动态组件写input是可以和v-model配合使用的。刚一开始我是那样写的,但是实现不了,我初步猜测可能是v-model从以前的value改成modelValue有关,所以我手动写事件实现了相似效果。如果后面知道了具体原因,会再次更新这篇文章的。
结语
结束了。