自定义组件(二)

透传 Attributes

“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 propsemits 的 attribute 或者 v-on 事件监听器。最常见的例子就是 classstyleid。以之前的customButton组件为例:

<!--customButton-->
<button @click="addClick">{{title}}:{{likes}}</button>

一个父组件使用了这个组件,并且传入了 class

<custom-button @addEvent="callback" class="large" title="点赞数" :likes="likes"/>

渲染之后

<button class="large">点赞数:8</button>

这里,customButton 并没有将 class 声明为一个它所接受的 prop,所以 class 被视作透传 attribute,自动透传到了customButton 的根元素上。

对 class 和 style 的合并

如果一个子组件的根元素已经有了 class 或 style attribute,它会和从父组件上继承的值合并。

<!--customButton-->
<button @click="addClick" class="btn" >{{title}}:{{likes}}</button>

则最后渲染出的 DOM 结果会变成:

<button class="btn large">点赞数:8</button>

v-on 监听器继承

同样的规则也适用于v-on事件监听器:

<custom-button @addEvent="callback" @click="onClick" class="large" title="点赞数" :likes="likes"/>

click 监听器会被添加到 customButton的根元素,即那个原生的 <button> 元素之上。当原生的 <button> 被点击,会触发父组件的 onClick 方法。

同样的,如果原生 button 元素自身也通过 v-on 绑定了一个事件监听器,则这个监听器和从父组件继承的监听器都会被触发。

禁用 Attributes 继承

如果你不想要一个组件自动地继承 attribute,你可以在组件选项中设置 inheritAttrs: false。如果你使用了 <script setup>,你需要一个额外的<script>块来书写这个选项声明:

<script>
// 使用普通的 <script> 来声明选项
export default {
  inheritAttrsfalse
}
</script>

<script setup>
/
/ ...setup 部分逻辑
</
script>

这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs 访问到。

<button @click="addClick">{{title}}:{{likes}}</button>
<span>Fallthrough attribute: {{ $attrs }}</span>

这个 $attrs 对象包含了除组件所声明的 propsemits 之外的所有其他 attribute,例如 classstylev-on 监听器等等。

JavaScript 中访问透传 Attributes

<script setup> 中使用useAttrs()API 来访问一个组件的所有透传 attribute:

<script setup>
import { useAttrs } from 'vue'

const attrs = useAttrs()
</script>

如果没有使用 <script setup>attrs 会作为 setup() 上下文对象的一个属性暴露:

export default {
  setup(props, ctx) {
    // 透传 attribute 被暴露为 ctx.attrs
    console.log(ctx.attrs)
  }
}

插槽 Slots

Vue 实现了一套用于向子组件分发内容的 API,也就是 <slot> 机制。

插槽内容与出口

举例来说,这里有一个 <FancyButton> 组件,可以像这样使用:

<FancyButton>
  Click me! <!-- 插槽内容 -->
</FancyButton>

<FancyButton> 的模板是这样的:

<button class="fancy-btn">
  <slot></slot> <!-- 插槽出口 -->
</button>

<slot> 元素是一个插槽出口 (slot outlet),标示了父元素提供的插槽内容 (slot content) 将在哪里被渲染。

插槽内容可以是任意合法的模板内容,不局限于文本。例如我们可以传入多个元素,甚至是组件:

<FancyButton>
  <!-- 添加一个Font Awesome 图标 -->
  <i class="fas fa-plus"></i>
  Click me!
</FancyButton>

或其他组件:

<FancyButton>
  <!-- 添加一个图标的组件 -->
  <font-awesome-icon name="plus"></font-awesome-icon>
  Click me!
</FancyButton>

渲染作用域

插槽内容可以访问到父组件的数据作用域,因为插槽内容本身是在父组件模板中定义的。举例来说:

<span>{{ message }}</span>
<FancyButton>{{ message }}</FancyButton>

这里的两个 {{ message }} 插值表达式渲染的内容都是一样的。

父组件模板中的表达式只能访问父组件的作用域;子组件模板中的表达式只能访问子组件的作用域。

插槽默认内容

在外部没有提供任何内容的情况下,可以为插槽指定默认内容。例如在一个 <submit-button> 组件中:

<button type="submit">
  <slot></slot>
</button>

我们希望这个 <button> 绝大多数情况下都渲染文本“Submit”。为了将“Submit”作为默认内容,我们可以将它放在<slot>标签内:

<button type="submit">
  <slot>
    Submit <!-- 默认内容 -->
  </slot>
</button>

现在当我们在一个父级组件中使用 <submit-button> 并且不提供任何插槽内容时:

<submit-button></submit-button>

备用内容“Submit”将会被渲染:

<button type="submit">Submit</button>

如果我们提供了插槽内容:

<submit-button>Save</submit-button>

则这个提供的内容将会被渲染从而取代默认内容:

<button type="submit">Save</button>

命名插槽

有时我们需要多个插槽。例如对于一个带有如下模板的 <base-layout> 组件:

<div class="container">
  <header>
    <!-- 我们希望把页头放这里 -->
  </header>
  <main>
    <!-- 我们希望把主要内容放这里 -->
  </main>
  <footer>
    <!-- 我们希望把页脚放这里 -->
  </footer>
</div>

对于这样的情况,<slot> 元素有一个特殊的属性:name。这个属性就是用来区分各个插槽的:

<div class="container">
  <header>
    <slot name="header"></slot>
  </header>
  <main>
    <slot></slot>
  </main>
  <footer>
    <slot name="footer"></slot>
  </footer>
</div>

不带 name<slot> 插槽的隐含名为default。在向命名插槽提供内容的时候,我们可以在外层包裹一个 <template> 元素,并在其上使用 v-slot 指令,然后以 v-slot 参数的形式提供插槽名称,如下所示:

v-slot 有对应的简写 #,因此 <template v-slot:header> 可以简写为 <template #header>。其意思就是“将这部分模板片段传入子组件的 header 插槽中”。

<base-layout>
  <template v-slot:header>
    <h1>Here might be a page title</h1>
  </template>
  <!--defaul可以默认不写-->
  <template v-slot:default>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </template>

  <template v-slot:footer>
    <p>Here's some contact info</p>
  </template>
</base-layout>

或者

<base-layout>
  <template #header>
    <h1>Here might be a page title</h1>
  </template>
    <!--defaul可以默认不写-->
  <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>
</base-layout>

<template> 元素中的所有内容都将会被传入相应的插槽。最后渲染出来的 HTML 将会是:

<div class="container">
  <header>
    <h1>Here might be a page title</h1>
  </header>
  <main>
    <p>A paragraph for the main content.</p>
    <p>And another one.</p>
  </main>
  <footer>
    <p>Here's some contact info</p>
  </footer>
</div>

作用域插槽

前面我们说了,插槽的内容有作用域,父子间不可以互相访问。

但是有时候,让插槽内容能够访问子组件中的数据是很有用的。

比如这个需求:当一个组件被用来渲染一个项目数组时,我们希望能够自定义每个项目的渲染方式。

这里我们来看一个 <FancyList> 组件的例子。它会渲染一个列表,并同时会封装一些加载远端数据的逻辑、使用数据进行列表渲染、或者是像分页或无限滚动这样更进阶的功能。然而我们希望它能够保留足够的灵活性,将对单个列表元素内容和样式的控制权留给使用它的父组件。我们期望的用法可能是这样的:

<FancyList :api-url="url" :per-page="10">
  <template #item="{ body, username, likes }">
    <div class="item">
      <p>{{ body }}</p>
      <p>by {{ username }} | {{ likes }} likes</p>
    </div>
  </template>
</FancyList>

<FancyList> 之中,我们可以多次渲染 <slot> 并每次都提供不同的数据 (注意我们这里使用了 v-bind 来传递插槽的 props):

<template>
  <div>
    <ul>
      <li v-for="item in items" :key="item">
        <slot name="item" v-bind="item"></slot>
      </li>
    </ul>
  </div>

</template>

<script setup>
// eslint-disable-next-line no-undef
  defineProps(['items'])
</script>

依赖注入

通常,当我们需要从父组件向子组件传递数据时,我们使用 props

但是,想象一下这样的结构:有一些深度嵌套的组件,而深层的子组件只需要父组件的部分内容。在这种情况下,如果仍然将 prop 沿着组件链逐级传递下去,可能会很麻烦。

对于这种情况,我们可以使用一对 provideinject。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。

这个特性有两个部分:父组件有一个 provide 选项提供数据,子组件有一个 inject 选项使用这些数据。

Provide (提供)

要为组件后代提供数据,需要使用到 provide() 函数:

<script setup>
import { provide } from 'vue'

provide(/* 注入名 */ 'message'/* 值 */ 'hello!')
</script>

如果不使用 <script setup>,请确保provide()是在setup()同步调用的:

import { provide } from 'vue'

export default {
  setup() {
    provide(/* 注入名 */ 'message'/* 值 */ 'hello!')
  }
}

provide() 函数接收两个参数。第一个参数被称为注入名,可以是一个字符串或是一个 Symbol。后代组件会用注入名来查找期望注入的值。一个组件可以多次调用 provide(),使用不同的注入名,注入不同的依赖值。

第二个参数是提供的值,值可以是任意类型,包括响应式的状态,比如一个 ref:

import { ref, provide } from 'vue'

const count = ref(0)
provide('key', count)

取个例子,定义一个父组件,子组件Child,子子组件GrandChild

父组件:

<template>
  <div>
    <input v-model="message">
    <Child></Child>
  </div>
</template>

<script setup>
import {provide, ref} from "vue";
import Child from "@/components/Child";

  const message = ref('hello')
  provide('message',message)
</script>

子组件Child:

<template>
  <grand-child></grand-child>
</template>

<script setup>
import GrandChild from './GrandChild.vue'
</script>

子子组件GrandChild

<template>
  <p> Message to grand child: {{ message }}</p>
</template>

<script>

</script>

目前运行结果:

自定义组件(二)
image.png

Inject (注入)

要注入上层组件提供的数据,需使用inject()函数:

<script setup>
import { inject } from 'vue'

const message = inject('message')
</script>

同样的,如果没有使用 <script setup>inject() 需要在 setup() 内同步调用:

import { inject } from 'vue'

export default {
  setup() {
    const message = inject('message')
    return { message }
  }
}

注入上面的子组件GrandChild

<template>
  <p> Message to grand child: {{ message }}</p>
</template>

<script setup>
  import {inject} from "vue";

  const message = inject('message')
</script>

运行结果:

自定义组件(二)
image.png
注入默认值

在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:

const value = inject('key', () => new ExpensiveClass())
和响应式数据配合使用

当提供 / 注入响应式的数据时,建议尽可能将任何对响应式状态的变更都保持在供给方组件中。这样可以确保所提供状态的声明和变更操作都内聚在同一个组件内,使其更容易维护。

有的时候,我们可能需要在注入方组件中更改数据。在这种情况下,我们推荐在供给方组件内声明并提供一个更改数据的方法函数:

<!-- 在供给方组件内 -->
<script setup>
import { provide, ref } from 'vue'

const location = ref('North Pole')

function updateLocation({
  location.value = 'South Pole'
}

provide('location', {
  location,
  updateLocation
})
</script>

<!-- 在注入方组件 -->
<script setup>
import { inject } from 'vue'

const { location, updateLocation } = inject('location')
</script>


<template>
  <button @click="updateLocation">{{ location }}</button>
</template>

最后,如果你想确保提供的数据不能被注入方的组件更改,你可以使用readonly()来包装提供的值。

<script setup>
import { ref, provide, readonly } from 'vue'

const count = ref(0)
provide('read-only-count', readonly(count))
</script>
使用 Symbol 作注入名

至此,我们已经了解了如何使用字符串作为注入名。但如果你正在构建大型的应用,包含非常多的依赖提供,或者你正在编写提供给其他开发者使用的组件库,建议最好使用 Symbol 来作为注入名以避免潜在的冲突。

我们通常推荐在一个单独的文件中导出这些注入名 Symbol:

// keys.js
export const myInjectionKey = Symbol()
// 在供给方组件中
import { provide } from 'vue'
import { myInjectionKey } from './keys.js'

provide(myInjectionKey, { /*
  要提供的数据
*/
 });
// 注入方组件
import { inject } from 'vue'
import { myInjectionKey } from './keys.js'

const injected = inject(myInjectionKey)

观看更多相关文章请关注公众号:

自定义组件(二)
前端编程.jpg


原文始发于微信公众号(大前端编程教学):自定义组件(二)

版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。

文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/224270.html

(0)
小半的头像小半

相关推荐

发表回复

登录后才能评论
极客之音——专业性很强的中文编程技术网站,欢迎收藏到浏览器,订阅我们!