设计模式
容器/展示模式
2015 年,Dan Abramov 撰写了一篇名为 “展示型组件和容器型组件” 的文章,改变了许多开发人员对 React 中组件架构的看法。他介绍了一种模式,将组件分为两类
- 展示型组件(或哑组件):这些组件关注事物的外观。它们不指定数据如何加载或修改,而是仅通过 props 接收数据和回调函数。
- 容器型组件(或智能组件):这些组件关注事物的工作原理。它们为展示型组件或其他容器型组件提供数据和行为。
虽然这种模式主要与 React 相关,但其基本原理已在其他库和框架中被采用并以各种形式进行了调整。
Dan 的区分提供了一种更清晰、更可扩展的方式来构建 JavaScript 应用程序。通过明确定义不同类型组件的职责,开发人员可以确保 UI 组件(展示型)和逻辑(容器型)更好地可重用。这个想法是,如果我们想要改变某件事的外观(例如按钮的设计),我们可以做到这一点,而无需触及应用程序的逻辑。反之,如果我们需要改变数据流或数据处理方式,展示型组件将保持不变,确保 UI 保持一致。
然而,随着 React 中 钩子 和 Vue 3 中 组合式 API 的出现,展示型组件和容器型组件之间的清晰界限开始变得模糊。钩子和组合式 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。
1<template>2 <DogImages :dogs="dogs" />3</template>45<script setup>6import { ref, onMounted } from "vue";7/* eslint-disable-next-line no-unused-vars */8import DogImages from "./DogImages.vue";910const dogs = ref([]);1112onMounted(async () => {13 const response = await fetch(14 "https://dog.ceo/api/breed/labrador/images/random/6"15 );16 const { message } = await response.json();17 dogs.value = message;18});19</script>
可组合函数
请阅读 可组合函数 指南,深入了解可组合函数。
在很多情况下,容器/展示模式可以用可组合函数代替。可组合函数的引入使开发人员能够轻松地添加状态,而无需容器型组件来提供该状态。
与其在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
组件中修改该数据。
在进行了所有更改之后,我们的应用程序看起来如下。
1import { ref, onMounted } from 'vue';23export default function useDogImages() {4 const dogs = ref([]);56 onMounted(async () => {7 const response = await fetch("https://dog.ceo/api/breed/labrador/images/random/6");8 const { message } = await response.json();9 dogs.value = message;10 });1112 return { dogs };13}
可组合函数使我们在组件中轻松地分离逻辑和视图,就像容器/展示模式一样。它节省了我们在容器型组件中包装展示型组件所需的额外层。