vue3学习组件之高级使用

32 篇文章 2 订阅

简言

在以前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>

效果:
示例2

动态组件

通过 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有关,所以我手动写事件实现了相似效果。如果后面知道了具体原因,会再次更新这篇文章的。

结语

结束了。

  • 1
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
Vue 2 中的高级组件开发可以通过 mixin、插槽和自定义指令等技术来实现。下面是一些常见的技巧: 1. Mixin:Mixin 是一种重用组件逻辑的方式。通过将常用的选项合并到 mixin 对象中,然后在组件使用 mixins 选项来引入 mixin。这样可以有效地提高代码的复用性。例如,您可以创建一个 mixin 对象,包含一些常用的方法和生命周期钩子函数,然后在需要的组件中引入。 2. 插槽(Slot):插槽是一种在父组件中向子组件传递内容的方式。通过在父组件使用<slot>标签,并在子组件使用<slot>标签的 name 属性来定义插槽。这样可以实现组件的灵活性,使父组件能够根据需要向子组件传递不同的内容。 3. 自定义指令(Custom Directive):自定义指令是一种在 HTML 元素上添加特定行为的方式。通过使用 Vue.directive 函数来定义一个全局的指令,然后在模板中使用 v-directive 指令来调用。自定义指令可以用于处理 DOM 元素的事件、样式、属性等。 4. 动态组件(Dynamic Component):动态组件允许您根据条件渲染不同的组件。通过使用<component>标签并通过 is 属性来指定要渲染的组件,可以根据需要切换不同的组件。 5. 渲染函数(Render Function):Vue 2 中可以使用 render 函数来编写组件的模板。使用 render 函数可以实现更灵活、动态的组件渲染。通过编写 JavaScript 代码来生成组件的虚拟 DOM 树,并将其返回给 Vue 实例进行渲染。 这些是一些基本的高级组件开发技巧,Vue 2 还提供了更多强大的特性和工具,您可以根据具体需求进一步深入学习和应用。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

ZSK6

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值