不同框架组件化之间的对比 之 slot

2,094 阅读6分钟

插: 刺肉也。从手从臿。

槽: 畜獸之食器。从木曹聲。

"插槽"来源于组件化。有些框架没有实现 slot, 但是也有类型 slot 的功能, React 的 props 功能强大可以传递 jsx 和 children 可以理解为"插槽"。

在前端中实现的了组件不只有 React/Vue/Angular/Svelte/小程序,其实还有 WebComponent。

WebComponent 是一套不同的技术,它的目的就是在于复用模板、样式和逻辑。

简介 WebComponent

WebComponent 由三种技术组成:

  1. 自定义元素 Custom element
  2. 影子 DOM
  3. 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 的插槽的内容:

  1. 默认插槽
<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>
  1. 插槽的退回机制

一般用于提示空插槽,没有插入任何内容 用在 slot 组件之内

  1. 命名插槽

类似于 Vue 和 WebComponent 的作用域插槽

  1. 插槽数据通过 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>

参考

  1. Web Components 是一套不同的技术,允许您创建可重用的定制元素(它们的功能封装在您的代码之外)并且在您的 web 应用中使用它们。
  2. 组件允许你将 UI 拆分为独立可复用的代码片段,并对每个片段进行独立构思。本指南旨在介绍组件的相关理念。

todo

  • 更好的例子补充