透传 Attributes
“透传 attribute”指的是传递给一个组件,却没有被该组件声明为 props
或 emits
的 attribute 或者 v-on
事件监听器。最常见的例子就是 class
、style
和 id
。以之前的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 {
inheritAttrs: false
}
</script>
<script setup>
// ...setup 部分逻辑
</script>
这些透传进来的 attribute 可以在模板的表达式中直接用 $attrs
访问到。
<button @click="addClick">{{title}}:{{likes}}</button>
<span>Fallthrough attribute: {{ $attrs }}</span>
这个 $attrs
对象包含了除组件所声明的 props
和 emits
之外的所有其他 attribute,例如 class
,style
,v-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 沿着组件链逐级传递下去,可能会很麻烦。
对于这种情况,我们可以使用一对 provide
和 inject
。无论组件层次结构有多深,父组件都可以作为其所有子组件的依赖提供者。
这个特性有两个部分:父组件有一个 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>
目前运行结果:
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>
运行结果:
注入默认值
在一些场景中,默认值可能需要通过调用一个函数或初始化一个类来取得。为了避免在用不到默认值的情况下进行不必要的计算或产生副作用,我们可以使用工厂函数来创建默认值:
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)
观看更多相关文章请关注公众号:
原文始发于微信公众号(大前端编程教学):自定义组件(二)
版权声明:本文内容由互联网用户自发贡献,该文观点仅代表作者本人。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌侵权/违法违规的内容, 请发送邮件至 举报,一经查实,本站将立刻删除。
文章由极客之音整理,本文链接:https://www.bmabk.com/index.php/post/224270.html