插: 刺肉也。从手从臿。
槽: 畜獸之食器。从木曹聲。
"插槽"来源于组件化。有些框架没有实现 slot, 但是也有类型 slot 的功能, React 的 props 功能强大可以传递 jsx 和 children 可以理解为"插槽"。
在前端中实现的了组件不只有 React/Vue/Angular/Svelte/小程序,其实还有 WebComponent。
WebComponent 是一套不同的技术,它的目的就是在于复用模板、样式和逻辑。
简介 WebComponent
WebComponent 由三种技术组成:
- 自定义元素 Custom element
- 影子 DOM
- HTML 模板
自定义元素和影子 DOM,都提供了一套 JavaScript 用于创建元素和 DOM。而 HTML 模板则相应的增加了 template 和 slot。
故事就要从 slot 开始时,具体的 webComponent 的创建过程:
创建一个组件模板:
<template id="my-app">
<!-- 内容 -->
<p>My paragraph</p>
<!-- 添加 有 name 的插槽 -->
<slot name="left"></slot>
<slot name="right"></slot>
<!-- 添加样式 -->
<style>
p {
color: red;
font-size: 20px;
}
</style>
</template>
<my-app>
<span slot="left">I`m left!</span>
<span slot="right">I`m right!</span>
</my-app>
注意:插槽是组件化的重要的做成部分,因为插槽增加的组件的灵活度。
my-app 是一个自定的组件,组件的内容就是插槽 slot。
我们将创建的内容,通过 js 挂载上 dom:
// 使用 id 获取模板
const template = document.getElementById("#my-app");
const tplContent = template.content;
document.body.appendChild(tplContent);
使用 webcompnent 相关的 api 创建组件:
- 使用模板
- 使用 shadow-dom
customElements.define(
"my-app",
class extends HTMLElement {
constructor() {
super();
let tpl = document.getElementById("#my-app");
let tplContent = tpl.content;
const shadowRoot = this.attachShadow({ mode: "open" }).appendChild(
templateContent.cloneNode(true)
);
}
}
);
我们看到 define 的第二个参数是一类,继承了 HTMLElement,在类的构造函数中获取了 tplContent 和将 cloneNode 方法将模板内容克隆到阴影的根节点上去。
React 的 “Props 插槽”
我们可以使用熟悉的脚手架来初始化一个项目:
- Gatsby 一个 GraphQL 获取数据脚手架工具,如果你对 GraphQL 感兴趣可以尝试。
- CreateReactApp 是 React 官方的脚手架
- 还有其他的 umijs、nextjs 等等优秀的脚手架,快速的开始一个 React 项目
React 重要的特点: 所有 React 组件都必须像“纯函数”一样保护它们的 props 不被更改。
Props 的可能类型,其实我们可以通过 PropTypes 这个包得到 Props 到底有哪些可能,因为他就是专门来验证 props。
import PropTypes from "prop-types";
注意在 React 15.5 版本中,已经将 prop-types 单独的发了一包,React 不在内置 props 类型校验。
props 类型校验在 Flow/TypeScript 中,可以使用 Flow/TypeScript 的强类型进行校验。
-
PropTypes.array 数组类型
-
PropTypes.bool 布尔类型
-
PropTypes.func 函数类型, 可能是函数组件
-
PropTypes.number 数值类型
-
PropTypes.object 对象类型 这个对象类型,可能就是我们的 jsx
-
PropTypes.string 字符串
-
PropTypes.symbol 符号类型
-
PropTypes.node 节点: 任何可被渲染的元素 (包括数字、字符串、元素或数组)
-
PropTypes.element 一个 React 元素
-
PropTypes.elementType 一个 React 元素类型
-
PropTypes.instanceOf(Message) 实例属性
-
PropTypes.oneOf(['a', 'b']) 指定值
-
PropTypes.oneOfType([ PropTypes.string, PropTypes.number, PropTypes.instanceOf(Message) ]) 指定类型
-
PropTypes.arrayOf(PropTypes.number) 数组成员 number 类型
-
PropTypes.objectOf(PropTypes.number) 对象的成员类型
-
PropTypes.shape({ color: PropTypes.string, fontSize: PropTypes.number }) 特定的数值类型
-
PropTypes.exact({ name: PropTypes.string, quantity: PropTypes.number }) 额外类型警告
-
PropTypes.func.isRequired 必须性
-
PropTypes.any.isRequired 任意类型必须性
-
...
我们看了 props 的支持类型是丰富的,可是函数,元素,组件,这意味着我们可以传递 jsx 当做插槽功能:
我们使用 Gatsby 来创建一个项目,创建
- page: slot
- component: MySlot
// page: slot.jsx
import React from "react";
// components
import MySlot from "../components/slot";
const slot = props => {
return (
<div>
<MySlot
left={<div>我们是slot 的 left</div>}
right={<div>我们是slot 的 right</div>}
>
<div>我是children内容1</div>
<div>我是children内容2</div>
<div>我是children内容3</div>
</MySlot>
</div>
);
};
export default slot;
// component: MySlot.jsx
import React from "react";
const MySlot = props => {
// 结构 props, 获取 “插槽”
const { left, right, children } = props;
return (
<div style={{ color: "#fff" }}>
<div style={{ backgroundColor: "blue" }}>left --- {left && left}</div>
<br />
<div style={{ backgroundColor: "red" }}>right --- {right && right}</div>
<br />
<div style={{ backgroundColor: "yellow", color: "#000" }}>
children --- {children && children}
</div>
</div>
);
};
export default MySlot;
我们将 MySlot 通过 props.left/props.right/props.children 传递三种“命名插槽”,在使用的时候,React 中不需要专门的 slot 元素 + name 属性进行标记,而是直接通过 props 进行访问 jsx 的内容,或者 React 组件。
Vue
如果看了 React Props 传递灵活性,你可能会想 Vue 的 Props 是不是也是这样灵活,毕竟一个 props 处理外部传入的组件的基本上所有问题。
Vue 支持的数据类型:
- String
- Number
- Boolean
- Array
- Object
- Date
- Function
- Symbol
尽管在 Vue 中可以把函数作为 prop 传递,但它被认为是一种反模式。一般不使用函数作为 props。而是使用事件的方式,进行数据传递,组件监听事件获取数据。
在 Vue 中 props 基本上不使用函数,虽然是可运行,但是 Vue 中事件系统,似乎更适合处理父子组件的数据传递。
Vue 支持的开发模式:
- 模板开发模式
- JSX 开发模式
- render 函数开发模式
虽然 render 会将 createElement 生成一个个的 VNode 节点,但是我们依然不考虑使用 props 直接传入 VNode 方式开发。所以 Vue 中使用 Props 传递组件、元素的方式,在 Vue 中是不自然的。
其实 Vue slot 设计就是参考 webComponent 插槽设计。但是 Vue 插槽系统更加灵活。
- 插槽
- 作用域插槽
在 2.6.0 版本中统一了指令 v-slot
来统一指令的写法
默认插槽
<nuxt-link url='/profile'>
<span>重视教育</span>
</nuxt-link>
<span>重视教育</span>
就是默认插槽
<!-- nuxt-link -->
<template>
<div>
<slot />
</div>
</template>
使用 render 函数渲染 默认插槽内容:
访问 vm 实例的 $slots
属性的默认值
export default {
render(h) {
return this.$slots.default;
}
};
命名插槽
<nuxt-link url="/profile">
<template v-slot:left>
<span>我是左边的插槽的内容</span>
</template>
<template v-slot:default>
<span>默认的插槽内容</span>
</template>
<template v-slot:right>
<p>我是右边的插槽内容</p>
</template>
</nuxt-link>
nuxt-link
<template>
<div class="container">
<header>
<slot name="left"></slot>
</header>
<main>
<slot></slot>
</main>
<footer>
<slot name="right"></slot>
</footer>
</div>
</template>
vue 中插槽数据的渲染特点
首先要明确一点: 各司其职,父组件只管理父组件数据,子组件只管理子组件的数据
父级模板里的所有内容都是在父级作用域中编译的;子模板里的所有内容都是在子作用域中编译的。
<template>
<app-link url="/profile">
Logged in as {{ user.name }}
</a-link>
</template>
<script>
export default {
data() {
return {
user: {
name: 'magnesium-'
}
}
}
}
</script>
Logged in as {{ user.name }}
渲染的是父组件的 magnesium-, 而不是 app-link
组件中的 user.name 属性。
作用域插槽
作用域插槽解决什么问题?其实就是 Vue 中在父组件中插槽数据,需要从子组件中获取。但是我们 Vue 父组件数据是单独分开的。所以 Vue 考虑到数据传递问题,提供了作用域插槽。
current-user>
<template v-slot:default="slotProps">
{{ slotProps.user.firstName }}
</template>
</current-user>
Vue 中插槽的主要内容就是:
- 默认插槽
- 具名插槽
- 作用域插槽
- 插槽指令
Angular 中插槽
Angular 投影 ng-content
类似于 Vue slot 使用 name 进行区分
<app-parent>
<app-child class="red"></app-child>
<app-child name="child"></app-child>
</app-parent>
<div style="background: cyan">
<ng-content select="[name=child]"></ng-content>
</div>
<div style="background: pink">
<ng-content select=".red"></ng-content>
</div>
装饰配合: @ContentChild
Angular 模板 ng-template
模板 ng-template 不使用不会真正的渲染为 html
配合装饰器:@ViewChild
TemplateRef
类似于 React/Vue 中 ref 对 dom 元素节点的引用。
ng-container
Svelte
Svelte 的插槽的内容:
- 默认插槽
<div class="box">
<slot>
<span style="color: 'red'">这个插槽没有任何内容啊</span>
</slot>
</div>
<script>
import App from './App.svelte';
</script>
<App>
<h2>Hello,</h2>
<p>World!</p>
</App>
- 插槽的退回机制
一般用于提示空插槽,没有插入任何内容 用在 slot 组件之内
- 命名插槽
类似于 Vue 和 WebComponent 的作用域插槽
- 插槽数据通过 prop 传递
Svelte 中 slot 的数据通过 props 来进行传递。Svelte 中 props 都需要需要声明。
<div>
<slot isGood="{isGood}"></slot>
</div>
<script>
import App from "./App.svelte";
</script>
<App let:isGood={isGood}>
<div>
{#if isGood}
<p>I am being hovered upon.</p>
{:else}
<p>Hover over me!</p>
{/if}
</div>
</App>
参考
- Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。。
- 组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。本指南旨在介绍组件的相关理念。
todo
- 更好的例子补充