容器/展示模式
2015 年,Dan Abramov 写了一篇标题为 展示和容器组件(Presentational and Container Components )的文章,改变了很多开发者在 React 中组件架构的思考方式。他引入了一种新的模式,这种模式将组件分为了两类:
展示组件(或哑组件、无状态组件): 这类组件只关注元素如何展示。它们不关心数据如何加载如何变化,只通过 props 接收数据和回调方法。
容器组件(或智能组件): 它们关心一切如何运作。它们向展示组件或其它的容器组件提供数据和具体的行为。
虽然这种模式主要用在 React 上,但这其中的基本原理很快被其它库或框架以各种形式采用。
Dan 提供的这种组件分类方式提供了一种更清晰并可扩展的方式来构建 JavaScript 应用。通过明确定义不同类型的组件的职责,开发者可以确保展示组件和容器组件各司其职,具有更好的可复用性。如果我们需要改变某些组件的外观(例如按钮的外观),我们可以在不触及应用程序逻辑的情况下达成修改的目的。相反地,如果我们需要改变数据流向或是具体的处理逻辑,我们也可以保持展示组件中的逻辑不变,从而确保 UI 保持一致。
然而,随着 React 中的 hooks 和 Vue 3 中 组合式 API 的出现,展示组件和容器组件的界限变得越来越模糊。 Hooks 和组合式 API 开始允许开发者封装和复用状态和逻辑,而不必局限于基于类的组件或者是选项式 API。因此容器/展示模式不需要再像以前那样严格遵守了。话虽如此,我们仍然需要在本文中花一些时间来讨论一下这个模式,因为它在某些时候仍然很有用。
我们接下来要创建一个应用程序,获取 6 张狗狗图片,并将图片渲染到屏幕上。
为了遵循容器/展示模式,我们希望通过将这个过程分为两部分,以此达成关注点分离:
展示组件:它关注数据是如何展示给用户的。在这个例子中,就是狗图片的渲染。
容器组件:它关注展示给用户什么数据。在这个例子中,就是狗图片的获取。
使用 应用程序逻辑
获取狗狗图片,然而显示图片仅仅是用 视图
来处理。
展示组件
展示组件通过 props
获取到数据。它的主要功能就是按照我们期望的方式,简单地 显示它得到的数据
,包括使用什么样的样式来显示,但不需要对数据进行修改。
我们看一下这个现实狗狗图片的例子。当渲染狗狗图片时,我们仅需要将从接口中获取到的狗狗图片映射在组件上,并渲染它们。为此,我们可以创建一个 DogImages
组件,它通过 props 接收数据,并为用户呈现接收到的数据。
<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 需要一个状态来保存与 UI 相关的数据。它们接收到的数据并不会被它们修改。
展示组件从 容器组件 中收到它们的数据。
容器组件
容器组件的主要作用是向展示组件 传递 它们所需的 数据。容器组件不会渲染任何组件,除了那些需要当前容器组件中的数据的展示组件。因为容器组件不渲染任何视图,所以它们通常也不会包含任何的样式。
在我们的例子中,我们想要向 DogImages
这个展示组件传递狗狗图片。在开始之前,我们需要从一个外部接口中获取图片数据。我们需要先创建一个 容器组件 来获取数据,然后将数据传递到展示组件 DogImages
中,让展示组件将它们显示在屏幕上。我们要创建的这个容器组件叫作 DogImagesContainer
。
<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 等状态管理工具时,这时可以让容器组件直接和状态进行交互,根据具体需求获取或更改状态。这也让展示组件的功能更加纯粹,它不需要知晓更多的应用逻辑,只需要专注于基于它们所得到的属性来渲染界面即可。
在演练场中查看代码组合式
确保你已经阅读过了组合式,这样接下来才能对其深入了解。
大多情况下,容器/展示模式都可以被组合式替代。组合式的出现,让开发者开发展示组件变得十分简单,当使用组合式的时候, 不需要 容器组件为其组件提供状态。
不需要在 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 };
}
我们通常会将这种组合式函数称作「钩子 hook」。通过使用这个 hook,我们再也不需要使用 DogImagesContainer
这个容器组件来获取数据,再将数据发送给展示组件 DogImages
了。取而代之的是,我们可以直接在展示组件 DogImages
中使用这个 hook!
<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()
hook,我们仍然需要从视图中分离应用逻辑。我们只需要使用 useDogImages
hook 的返回值,不需要在 DogImages
组件中修改数据。
通过一番修改,我们的应用看上去像之前那样一样了。
在演练场中查看代码组合式让逻辑和视图分离这件事变得十分简单,就像容器/展示模式一样。组合式为我们节省了额外的一层程序逻辑。