目录
一、插槽 和 组件 对比
- 共同点:都可以被复用——从这一点来讲,插槽其实是组件的一种高级实现。
- 不同点:
- 插槽支持 使用者 对其本身的 定制化改造。
- 组件不支持 使用者 对其本身的 定制化改造。
- 适用场景:
- 插槽适用于:需要支持可定制化的复用组件的场景。
- 组件适用于:需要仅仅复用组件的场景。
二、v-slot 语法
原来的 vue 插槽有三种 :匿名插槽、具名插槽 和 作用域插槽。不过,在 vue2.6+ 之后采用 v-slot 指令来代替原来的三种插槽。
v-slot 的特性:
- v-slot 只能添加在 <template> 上,独占默认插槽除外。
- v-slot 属性可以简写成 “#插槽名称” 的方式
- v-slot 可以直接访问组件绑定的属性
- v-slot 代替作用域插槽的时候,子组件仍然需要绑定向父组件暴露的属性
三、插槽(slot 标签)
1、一言以蔽之什么是插槽?
插槽用 <slot></slot> 表示,它是一个:子组件提供给父组件的占位符,父组件可以在这个占位符中填充任何模板代码。
2、创建插槽
- 使用 <slot></slot> 标签可以创建一个插槽。
- <slot> 标签内可以包含任何模板代码,包括 HTML,甚至其它的组件。
- 可以给 <slot> 标签设置默认值,默认值只有在使用者没有提供任何内容的时 才会被渲染。
- 每个 <slot> 标签都会有一个默认的名字:“default”。
- 一个组件内若定义多个 <slot> 标签时,必须给这些 <slot> 标签定义不同的名字。
(1)、创建一个简单的插槽
使用 <slot></slot> 标签创建一个插槽:
<!-- 在 slot-component 组件中创建一个插槽-->
<template>
<slot></slot>
</template>
在其他组件中使用已创建好的插槽:
<template>
<slot-component> hello world </slot-component>
<!-- ... -->
</template>
上述代码中,当 slot-component 组件渲染时,<slot></slot> 标签将会被替换为 “hello world”。不过,若 slot-component 组件的 template 中没有包含一个 <slot> 元素时,则该部分会被抛弃而不被渲染。
(2)、给插槽设置默认值
插槽的默认值只会在没有提供内容的时候才会被渲染。
例如:假设在 submit-button 组件中定义了一个插槽:
<!-- submit-button 组件-->
<button type="submit">
<slot></slot>
</button>
我们可能希望这个 <button> 内绝大多数情况下都渲染文本“Submit”。于是,可以通过给这个 <slot> 标签设置默认值来实现:
<button type="submit">
<slot>Submit</slot>
</button>
然后在一个父级组件中使用 <submit-button> 组件:
<!-- 提供内容 Save 时 -->
<submit-button>Save</submit-button>
<!-- 渲染为:<button type="submit"> Save </button> -->
<!-- 不提供内容时 -->
<submit-button></submit-button>
<!-- 渲染为:<button type="submit"> Submit </button> -->
(3)、给插槽起名
每个 <slot> 标签都会有一个默认的名字叫做:“default”。
当我们需要在一个组件中定义多个插槽时,必须通过 <slot> 标签的 name 属性给 <slot> 起一个独一无二的名字。
例如:
<!-- base-layout 组件 -->
<div class="container">
<header>
<slot name="header"></slot>
</header>
<main>
<slot></slot>
<!-- 等价于 <slot name="default"></slot> -->
</main>
<footer>
<slot name="footer"></slot>
</footer>
</div>
在向具名插槽提供内容的时,可以在一个 <template> 元素上,通过使用 “v-slot:name” 指令(可以简写为 “#name”),将内容传递给指定名字的插槽。
<base-layout>
<template v-slot:header>
<h1>欢迎你来到这个页面</h1>
</template>
<p>1234567890</p>
<p>0987654321</p>
<!-- 这等价于:
<template v-slot:default>
<p>1234567890</p>
<p>0987654321</p>
</template>
-->
<template #footer>
<p>走吧走吧</p>
</template>
</base-layout>
现在 <template> 元素中的所有内容都将会被传入相应的插槽。任何没有被包裹在带有 v-slot 的 <template> 中的内容都会被视为 “默认插槽的内容”。
最终渲染为:
<div class="container">
<header>
<h1>欢迎你来到这个页面</h1>
</header>
<main>
<p>1234567890</p>
<p>0987654321</p>
</main>
<footer>
<p>走吧走吧</p>
</footer>
</div>
【注意】
v-slot 只能添加在 <template> 上,只有一种情况除外:独占默认插槽(见下文)。
3、给插槽分发内容
(1)、给多个插槽分发内容
当我们在组件中需要给多个插槽分发内容时,就需要:给分发内容的插槽模版 指定与之对应的插槽名字。(不能使用 “默认插槽的缩写语法”,因为这是在给 “具名插槽” 分发内容。)
语法:
v-slot:插槽的名字
// 或者
#插槽的名字
举个例子:
<!-- current-user 组件 -->
<template>
<slot name="one"></slot>
<slot name="two"></slot>
<!-- 这是一个默认插槽(没起名字) -->
<slot></slot>
</template>
<current-user>
<template v-slot:one">
<span>1</span>
</template>
<template #two>
<span>2</span>
</template>
<!-- 默认插槽会启用默认的名字:default,可省略 -->
<span>0</span>
<!-- 等价于
<template v-slot:default>
<span>0</span>
</template>
-->
</current-user>
(2)、给独占默认插槽分发内容
这里涉及了 插槽 prop,请先阅读完下文的 “插槽的作用域”,然后再回看这部分内容。
独占默认插槽指的是:被提供的内容只有默认插槽,没有具名插槽。
当给独占默认插槽分发内容时,组件的标签才可以被当作插槽的模板来使用(省去了 <template> 标签)。
例如:
<current-user v-slot:default="slotProps"> hello world </current-user>
还可以简写为:
<current-user v-slot="slotProps"> hello world </current-user>
【注意】
“默认插槽的缩写语法” 不能和 “具名插槽” 混用,因为它会导致作用域不明确:
<!-- 无效,会导致警告 -->
<current-user v-slot="slotProps">
{{ slotProps.user.firstName }}
<template v-slot:other="otherSlotProps">
slotProps is NOT available here
</template>
</current-user>
4、插槽的作用域
(1)、插槽跨域的产生
在使用插槽时,不在同一个组件中访问插槽的内容会产生插槽跨域。所以,插槽的作用域遵循一个原则:
- 父级模板里的所有内容都是在父级作用域中编译的;
- 子级模板里的所有内容都是在子级作用域中编译的。
举个反例:
<!-- current-user 组件 -->
<template>
<slot>{{ user.lastName }}</slot>
</template>
我们在使用 current-user 组件时,想换掉备用内容,用名而非姓来显示。如下:
<template>
<current-user> {{ user.firstName }} </current-user>
</template>
上述代码不会正常工作,因为只有 current-user 组件可以访问到 user,而我们提供的内容是在 current-user 组件的父组件中渲染的,这就会产生 插槽跨域。
一般情况下,在父组件中使用子组件时,无法访问子组件中插槽里的内容。这就是插槽作用域的界限。那么能否打破这个界限呢?
(2)、打破插槽的作用域界限——插槽 prop
通过使用插槽 prop 可以实现:在父组件中使用子组件时,能够访问到子组件中插槽里的内容。
插槽 prop——是一个包含了来自该 <slot> 元素上所有属性的对象。其中,这些属性是通过 v-bind 指令(可简写为 “:”)绑定在该 <slot> 元素上的。
语法:
v-slot:插槽的名字='插槽 prop 的名字'
// 或者
#插槽的名字='插槽 prop 的名字'
这里请聚焦 “插槽 prop 的名字”。
例如:
为了让 user 在父级模板中可用,我们可以在 current-user 组件里,将 user 作为 <slot> 元素的一个 attribute 绑定上去:
<!-- current-user 组件 -->
<template>
<slot v-bind:user="user"> {{ user.lastName }} </slot>
</template>
在父级模板中使用 current-user 组件时,通过自定义 v-slot 的值来给该插槽的 prop 起名。这里给插槽的 prop 起名为 slotProps。那么,slotProps 对象包含了所有来自该插槽的属性:
<current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
其实这一步相当于给插槽 prop 起名。
插槽 prop 的更多应用示例:
<!-- current-user 组件 -->
<template>
<slot name="one" :user="content"></slot>
<slot name="two" :user="info"></slot>
<!-- 这是一个默认插槽(没起名字) -->
<slot :user="datas"></slot>
</template>
<current-user>
<template v-slot:one="oneSlotProps">
{{oneSlotProps.name}}
</template>
<template #two="twoSlotProps">
{{twoSlotProps.age}}
</template>
<!-- 默认插槽会启用默认的名字:default -->
<template v-slot:default="defaultSlotprops">
{{defaultSlotprops.gender}}
</template>
</current-user>
(3)、插槽与组件的通信
方法与属性一样,都依赖于插槽 prop——在定义插槽的组件中将方法和属性抛出。在使用的组件中通过插槽 prop 接收后使用即可。
例如:
在子组件中产生:
<template lang='pug'>
.more-slot
slot(name='addSlot' :add='addHandle')
Button +
slot(name='numSlot')
span {{number}}
</template>
<script>
export default {
data () {
number: 0
},
methods: {
addHandle () {
this.numer++
}
}
}
</script>
父组件中消费掉:
<temolate lang='pug'>
more-slot
template(#addSlot='{add}')
span(@click='add()') 增加
template(#numSlot)
</template>
<script>
import MoreSlot from './more-slot/index.vue'
export default {
components: {
MoreSlot
}
}
</script>
渲染为:
(4)、作用域插槽的内部工作原理
作用域插槽的内部工作原理是将你的插槽内容包裹在一个拥有单个参数的函数里:
function (slotProps) {
// 插槽内容
}
这意味着 v-slot 的值实际上可以是任何能够作为函数定义中的参数的 JavaScript 表达式。
5、解构插槽 Prop
在作用域插槽的内部工作原理的支持下,在单文件组件或现代浏览器中,你也可以使用 ES2015(ES6)的 “解构” 语法来传入具体的插槽 prop。
例如:
<current-user v-slot="{ user, workInfo }">
{{ user.firstName }}
</current-user>
这样可以使模板更简洁,尤其是在该插槽提供了多个 prop 的时候。
(1)、解构插槽 Prop 重命名
例如,将 user 重命名为 person:
<current-user v-slot="{ user: person }">
{{ person.firstName }}
</current-user>
(2)、解构插槽 Prop 设置默认值
解构插槽 Prop 时,给解构的属性设置默认值是有必要的,此默认值只会在当插槽 prop 是 undefined 的时候生效:
<current-user v-slot="{ user = { firstName: 'Guest' } }">
{{ user.firstName }}
</current-user>
(3)、解构插槽 Prop 的应用
插槽 prop 允许我们将插槽转换为可复用的模板,这些模板可以基于输入的 prop 渲染出不同的内容。
例如,我们要实现一个 <todo-list> 组件,它是一个列表且包含布局和过滤逻辑:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
{{ todo.text }}
</li>
</ul>
我们可以将每个 todo 作为父级组件的插槽,以此通过父级组件对其进行控制,然后将 todo 作为一个插槽 prop 进行绑定:
<ul>
<li
v-for="todo in filteredTodos"
v-bind:key="todo.id"
>
<!--
我们为每个 todo 准备了一个插槽,
将 `todo` 对象作为一个插槽的 prop 传入。
-->
<slot name="todo" v-bind:todo="todo">
<!-- 后备内容 -->
{{ todo.text }}
</slot>
</li>
</ul>
现在当我们使用 <todo-list> 组件的时候,我们可以选择为 todo 定义一个不一样的 <template> 作为替代方案,并且可以从子组件获取数据:
<todo-list v-bind:todos="todos">
<template v-slot:todo="{ todo }">
<span v-if="todo.isComplete">✓</span>
{{ todo.text }}
</template>
</todo-list>
这只是作用域插槽用武之地的冰山一角。想了解更多现实生活中的作用域插槽的用法,我们推荐浏览诸如 Vue Virtual Scroller、Vue Promised 和 Portal Vue 等库。
6、动态插槽
通过将插槽的名字指定为一个变量,来动态的加载不同的插槽。
例如:
<base-layout>
<template v-slot:[dynamicSlotName]>
...
</template>
</base-layout>
四、插槽的原理
插槽的原理要从 Shadow DOM 谈起。
Shadow DOM 是普通的 DOM,但有两个区别:
- 它是如何创建/使用的;
- 它与页面其余部分相关的行为方式。
您创建 DOM 节点并将它们附加为另一个元素的子元素。使用 shadow DOM,您可以创建一个附加到元素的作用域 DOM 树,但与它的实际子元素分开。这个范围子树称为影子树。它附加到的元素是它的影子主机。您在阴影中添加的任何内容都会成为托管元素的本地元素,包括<style>. 这就是 shadow DOM 实现 CSS 样式范围的方式。
1、创建 shadow DOM
const header = document.createElement('header')
const shadowRoot = header.attachShadow({mode: 'open'})
shadowRoot.innerHTML = '<h1>Hello Shadow DOM</h1>'// 也可以使用 appendChild()。
Shadow DOM 使用<slot>元素将不同的 DOM 树组合在一起。 插槽是组件内的占位符,用户可以用自己的标记填充。通过定义一个或多个插槽,您可以邀请外部标记在组件的影子 DOM 中呈现。本质上,您是在说“在此处渲染用户的标记”。
2、为自定义元素创建 shadow DOM
Shadow DOM 在创建自定义元素时特别有用 。使用 shadow DOM 划分元素的 HTML、CSS 和 JS,从而生成“Web 组件”。
// 使用自定义元素 API v1 注册新的 HTML 标签并定义其 JS 行为
// 使用 ES6 类。 <fancy-tab> 的每个实例都将具有相同的原型。
customElements.define('fancy-tabs', class extends HTMLElement {
constructor() {
super(); // 总是在构造函数中首先调用 super()。
// 将影子根附加到 <fancy-tabs>。
const shadowRoot = this.attachShadow({mode: 'open'});
shadowRoot.innerHTML = `
<style>#tabs { ... }</style> <!-- 样式的范围是 fancy-tabs! -->
<div id="tabs">...</div>
<div id="panels">...</div>
`;
}
...
});
3、在 JS 中使用插槽
shadow DOM API 提供了 slotchange 事件——在插槽的分布式节点发生更改时触发。例如,如果用户在 light DOM 中添加/删除子项。
const slot = this.shadowRoot.querySelector('#slot');
slot.addEventListener('slotchange', e => {
console.log('子节点改变了!');
});
4、插槽中正在渲染哪些元素?
有时了解哪些元素与插槽相关联很有用。调用 slot.assignedNodes()以查找插槽正在渲染的元素。该 {flatten: true}选项还将返回插槽的后备内容(如果没有分发节点)。
例如,假设您的 shadow DOM 如下所示:
<slot><b>fallback content</b></slot>
用法 | 称呼 | 结果 |
---|---|---|
<my-component>组件文本</my-component> | slot.assignedNodes(); | [component text] |
<my-component></my-component> | slot.assignedNodes(); | [] |
<my-component></my-component> | slot.assignedNodes({flatten: true}); | [<b>fallback content</b>] |
5、元素被分配到哪个插槽?
element.assignedSlot告诉你你的元素被分配到哪个组件槽。
当一个事件从 shadow DOM 冒泡时,调用event.composedPath()将返回事件经过的节点数组。
6、使用自定义事件
在阴影树的内部节点上触发的自定义 DOM 事件不会冒泡出阴影边界,除非使用 composed: true标志创建事件:
// <fancy-tab> 自定义元素类定义里面
selectTab() {
const tabs = this.shadowRoot.querySelector('#tabs');
tabs.dispatchEvent(new Event('tab-select', {bubbles: true, composed: true}));
}