2015 年,Dan Abramov 写了一篇题为《展示和容器组件》的文章,改变了许多开发人员对 React 组件架构的思考方式。他介绍了一种将组件分为两类的模式:
- 展示组件(或哑组件):这些组件与外观有关。它们与数据如何加载或变化无关,而是仅仅通过 props 来接收数据和回调方法。
- 容器组件(或智能组件):这些组件与运行方式有关。它们向展示组件或其他容器组件提供数据和行为。
虽然这种模式主要与 React 相关,但其基本原则被其他库和框架以各种形式采用或通过其他形式呈现。
Dan 的杰出贡献为构建 JavaScript 应用提供了一种更清晰、更可扩展的方式。通过明确定义不同类型组件的职责,开发人员可以确保 UI 组件(展示)和逻辑(容器)具有更高的可重用性。我们的想法是,如果我们要改变某些东西的外观(例如按钮的设计),我们可以在不触及应用逻辑的情况下做到这一点。相反,如果我们需要更改数据的流向或处理方式时,展示组件将保持不变,从而确保 UI 的一致性。
然而,随着 React 中 hooks 和 Vue 3 中组合式 API 的出现,展示组件和容器组件之间的清晰界限开始变得模糊。 Hooks 和组合式 API 开始允许开发人员封装和重用状态和逻辑,而不必局限于基于类的容器组件或选项 API。因此,容器/展示模式不需要像以前那样恪守。话虽如此,我们仍将在本文中花一些时间讨论该模式,因为它有时会很有帮助。
假设我们想要创建一个应用程序来获取 6 张狗的图片,并将这些图片渲染到屏幕上。
为了遵循容器/展示模式,我们希望通过将此过程分为两部分来强制关注点分离:
- 展示组件:关心如何向用户显示数据的组件。在此示例中,这是狗图片列表的渲染。
- 容器组件:关心向用户显示哪些数据的组件。在此示例中,这是获取狗图片数据。
获取狗的图片处理应用逻辑,而展示图片仅处理视图。
展示组件
展示组件通过 props 接收数据。它的主要功能是简单地以我们希望的方式显示它接收到的数据(包括样式),而不修改这些数据。
让我们看一下显示狗图片的示例。渲染狗图片时,我们只想映射从 API 获取的每个狗图片并渲染这些图片。为此,我们可以创建一个 DogImages 组件,该组件通过 props 接收数据并渲染接收到的数据。
<!-- DogImages.vue -->
<template>
<img v-for="(dog, index) in dogs" :src="dog" :key="index" alt="Dog" />
</template>
<script setup>
import { defineProps } from "vue";
const { dogs } = defineProps(["dogs"]);
</script>
DogImages 组件可以被认为是一个展示组件。展示组件通常是无状态的:它们不包含自己的组件状态,除非它们需要用于 UI 目的的状态。他们接收到的数据不会被展示组件本身改变。
展示组件从容器组件接收数据。
容器组件
容器组件的主要功能是将数据传递给它们所包含的展示组件。除了关联其数据的展示组件之外,容器组件本身通常不会渲染任何其他组件。由于它们本身不渲染任何内容,因此通常也不包含任何样式。
在我们的示例中,我们希望将狗图片传递到 DogsImages 展示组件。在此之前,我们需要从外部 API 获取图片。我们需要创建一个容器组件来获取这些数据,并将这些数据传递给展示组件 DogImages 以将其显示在屏幕上。我们将这个容器组件称为 DogImagesContainer 。
<!-- DogImagesContainer.vue -->
<template>
<DogImages :dogs="dogs" />
</template>
<script setup>
import { ref, onMounted } from "vue";
import DogImages from "./DogImages.vue";
const dogs = ref([]);
onMounted(async () => {
const response = await fetch(
"https://dog.ceo/api/breed/labrador/images/random/6"
);
const { message } = await response.json();
dogs.value = message;
});
</script>
将这两个组件组合在一起可以将处理应用程序逻辑与视图分开。
简而言之,这就是容器/表示模式。当与 Pinia 等状态管理解决方案集成时,可以利用容器组件直接与存储交互,根据需要获取或改变状态。这使得展示组件保持纯粹并且不需要更多地涉及程序逻辑,只专注于根据它们收到的 props 渲染 UI。
组合式函数
阅读 2.组合式函数 指南,深入了解组合式函数。
在许多情况下,容器/表示模式可以用组合式函数替换。组合式函数的引入使开发人员可以轻松添加状态,而无需容器组件来提供状态。
我们可以创建一个组合式函数来获取图片并返回狗的数组,而不是在 DogImagesContainer 组件中使用数据获取逻辑。
import { ref, onMounted } from "vue";
export default function useDogImages() {
const dogs = ref([]);
onMounted(async () => {
const response = await fetch(
"https://dog.ceo/api/breed/labrador/images/random/6"
);
const { message } = await response.json();
dogs.value = message;
});
return { dogs };
}
通过使用这个钩子,我们不再需要包装 DogImagesContainer 容器组件来获取数据并将其发送到展示 DogImages 组件。相反,我们可以直接在我们的演示 DogImages 组件中使用这个钩子!
<template>
<img v-for="(dog, index) in dogs" :src="dog" :key="index" alt="Dog" />
</template>
<script setup>
import useDogImages from "../composables/useDogImages";
/* eslint-disable-next-line no-unused-vars */
const { dogs } = useDogImages();
</script>
通过使用 useDogImages() 钩子,我们仍然将应用逻辑与视图分开。我们只是使用 useDogImages 钩子返回的数据,而不修改 DogImages 组件中的数据。
经过我们的所有更改,应用程序如下所示:
组合式函数可以轻松分离组件中的逻辑和视图,就像容器/展示模式一样。它为我们节省了将展示组件包装在容器组件中所需的额外层。