Published on

Vue - 无渲染组件

Authors
  • avatar
    Name
    Deng Hua
    Twitter

文章翻译至: Renderless components

目录

什么是无渲染组件(renderless components)

无渲染组件是 Vue 中的一种模式,它将组件的逻辑与表现分离。该模式提供了一种封装功能的方法,而无需指定组件的视觉表示。

换句话说,无渲染组件仅关注逻辑和行为,而将UI留给父组件。

当我们需要创建可应用于不同 UI 的可重用逻辑时,无渲染组件非常实用。通过将逻辑抽象为无渲染组件,我们可以轻松地在各种上下文中复用它,而无需重复写代码。

如果此时您仍然感到困惑,别担心!让我们通过一个例子更深入地探讨这个概念。

重复的逻辑

想象一下,您有一个toggle UI元素,需要在应用中的不同位置使用,但每个实例可能有不同的视觉表现。一些toggle可能显示为button,而另一些可能显示为checkboxswitch

我们当然可以为上面的示例创建三个不同的toggle组件,但是,我们也可以观察到每个toggle元素具有相同的逻辑和行为。

每个toggle都有一个非活动和活动状态,可以通过组件内的状态变量(例如 checked )进行跟踪。单击toggle按钮时,其组件状态将从非活动状态变为活动状态,反之亦然(即 checked = !checked )。

下面的视觉效果显示了每个组件的 <template><script> 块的构造方式:

看到这,我们立即可以想到的是,可以通过提取通用逻辑和行为来创建更通用的组件,这样我们就不必在每个单独的切换组件中重复定义状态和切换方法。这是使用可组合项的一个很好的例子,因为可组合项将允许我们在不同的toggle组件之间封装和共享通用的状态逻辑。

useCheckboxToggle:

import { ref } from "vue";

export function useCheckboxToggle() {
  const checkbox = ref(false);

  const toggleCheckbox = () => {
    checkbox.value = !checkbox.value;
  };

  return {
    checkbox,
    toggleCheckbox,
  };
}

切换组件:

<template>
  <div class="comp">
    <label class="switch">
      <input type="checkbox" :value="checkbox" @click="toggleCheckbox" />
      <div class="slider rounded" :class="checkbox ? 'active' : ''"></div>
    </label>
  </div>
</template>

<script setup>
  import { useCheckboxToggle } from "./composables/useCheckboxToggle";

  const { checkbox, toggleCheckbox } = useCheckboxToggle();
</script>

尽管上述内容非常适合我们的用例,但 Vue 为我们提供了另一种模式,告诉我们如何复用逻辑,同时保持其与视图的解耦。

无渲染组件

无渲染组件背后的主要思想是创建一个组件,该组件本身不渲染任何 HTML 或 UI 元素,但将其内部状态和方法公开给其父组件。然后,父组件根据无渲染组件的公开数据和行为渲染适当的 UI。

由父组件来决定渲染内容是什么,可以通过插槽来实现。

插槽允许父组件将模板内容注入到子组件中,类似于 props,不过它们并不传递 JavaScript 值,而是将模板片段传递给子组件。

让我们开始创建我们的toggle无渲染组件。在组件的 <script> 部分,我们首先创建toggle复选框的状态和状态切换的逻辑。

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

  const checkbox = ref(false);

  const toggleCheckbox = () => {
    checkbox.value = !checkbox.value;
  };
</script>

在组件的 <template> 部分中,我们将使用 <slot> 元素来表示这将是父组件传入的模板内容所在的位置。

<template>
  <slot></slot>
</template>

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

  const checkbox = ref(false);

  const toggleCheckbox = () => {
    checkbox.value = !checkbox.value;
  };
</script>

然后传入父组件的 checkboxtoggleCheckbox()属性。

<!-- ToggleComponent.vue -->
<template>
  <slot :checkbox="checkbox" :toggleCheckbox="toggleCheckbox"></slot>
</template>

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

  const checkbox = ref(false);

  const toggleCheckbox = () => {
    checkbox.value = !checkbox.value;
  };
</script>

在父组件中,我们可以引用checkboxtoggleCheckbox(),就像我们决定如何呈现子组件一样。

可以看到,我们创建的组件并没有自己的模板内容,这就是它称为无渲染组件的原因,该组件仅专注于逻辑和行为,将UI表现留给其父组件。

在父组件中,我们现在将尝试渲染三个不同的toggle元素,每个元素都有自己独特的用户体验。我们首先导入上面创建的无渲染 ToggleComponent 组件。

<script setup>
  import ToggleComponent from "./components/ToggleComponent";
</script>

现在,我们可以尝试渲染 <ToggleComponent> ,传入你需要显示的模板内容。

<template>
  <ToggleComponent>
    <!-- 插槽内容 -->
  </ToggleComponent>
</template>

<script setup>
  import ToggleComponent from "./components/ToggleComponent";
</script>

当我们渲染slot内容时,我们需要访问子组件上下文的属性( checkboxtoggleCheckbox() )。

由于我们之前已将这些属性传递给插槽 ,因此我们可以使用 v-slot 指令来接收这些插槽属性。

<template>
  <ToggleComponent v-slot="{ checkbox, toggleCheckbox }">
    <!-- 插槽内容 -->
  </ToggleComponent>
</template>

<script setup>
  import ToggleComponent from "./components/ToggleComponent";
</script>

注意: 这里是将定义<slot>所在组件的作用域内的变量和状态,传递给使用该插槽的组件所处的作用域。

如果看不懂的话,借用官方文档的代码和图来说明:

<!-- 定义<MyComponent>组件,内含一个插槽 -->
<div>
  <slot :text="greetingMessage" :count="1"></slot>
</div>

<!-- 使用<MyComponent>组件,这里接受插槽传递出来的数据 -->
<MyComponent v-slot="slotProps">
  {{ slotProps.text }} {{ slotProps.count }}
</MyComponent>


接着之前的示例代码。

有了可用的相关插槽和props,我们现在可以渲染第一个toggle元素。此toggle元素将是一个switch开关,根据 checkbox 属性的值从非活动状态切换到活动状态。

<template>
  <!-- 这里使用对象解构,等价 v-slot="slotProps" ..... slotProps.checkbox -->
  <ToggleComponent v-slot="{ checkbox, toggleCheckbox }">
    <div class="comp">
      <label class="switch">
        <input
          type="checkbox"
          :value="checkbox"
          @click="toggleCheckbox"
        />
        <div class="slider rounded" :class="checkbox ? 'active' : ''"></div>
      </label>
    </div>
  </ToggleComponent>
</template>

<script setup lang="ts">
import ToggleComponent from './ToggleComponent.vue';
</script>

保存更改时,我们将在应用程序中看到开关切换。

我们可以继续以相似的方式创建其他两个toggle元素。

第二个切换元素将是一个button,单击该按钮时,会在 Toggle | Yes 😀Toggle | No 😔 文本之间切换。

第三个Toggle元素将是两个选项卡式按钮,单击其中一个按钮即可切换两个按钮的活动状态。

最终三个Toggle元素的代码:

<template>
  <!-- Toggle Checkbox -->
  <ToggleComponent v-slot="{ checkbox, toggleCheckbox }">
    <div class="comp">
      <label class="switch">
        <input
          type="checkbox"
          :value="checkbox"
          @click="toggleCheckbox"
        />
        <div class="slider rounded" :class="checkbox ? 'active' : ''"></div>
      </label>
    </div>
  </ToggleComponent>

  <!-- Toggle Button -->
  <ToggleComponent v-slot="{ checkbox, toggleCheckbox }">
    <div class="comp">
      <button class="toggle-button" @click="toggleCheckbox">
        Toggle | <span>{{ checkbox ? "Yes 😀" : "No 😔" }}</span>
      </button>
    </div>
  </ToggleComponent>

  <!-- Toggle Tab -->
  <ToggleComponent v-slot="{ checkbox, toggleCheckbox }">
    <div class="comp">
      <button
        :class="['tab-button', { active: checkbox }]"
        @click="toggleCheckbox"
      >
        On
      </button>
      <button
        :class="['tab-button', { active: !checkbox }]"
        @click="toggleCheckbox"
      >
        Off
      </button>
    </div>
  </ToggleComponent>
</template>

<script setup lang="ts">
import ToggleComponent from './ToggleComponent.vue';
</script>

End.