译者前言
日常维护当中少不了一些技改需求,比如说重构xx,或者干脆开发的时候就留足扩展性,可维护性
项目内写屎山多吧? 如何避免屎山,可以学习一些设计模式,让代码更容易维护和扩展,但不利于防御性编程😀。 Container/Presentational Pattern (patterns.dev)
正文:
在2015年,Dan Abramov写了一篇名为“展示组件与容器组件”的文章,这改变了开发者对React当中组件结构的看法。他介绍了一种设计模式,能够将组件分为两种类型:
- 展示组件(或者说是哑巴组件)它们关心事物长啥样。不细究数据是如何装载和变化的,只通过props接受数据和回调函数
- 容器组件(或者说是聪明组件)它们只关心事物是如何工作的。它们提供数据和行为给展示或其他容器组件
此设计模式主要和React相关联,但它的基本原则被采纳和应用在不同种类的框架和库当中。
Dan与众不同的提议为构建javascript应用程序提供了1个更清晰、更可拓展的方式。 通过清晰的定义不同类型组件的职责,开发者可以确保UI组件(展示)和逻辑(容器组件)有更好的复用性。这个想法就是:如果我们改变某些东西的样子(比如按钮的设计),无需改动到这个App的逻辑,相反地,如果我们需要改变数据的流向或者处理,展示组件不会被改动,确保UI始终如一。
然而,伴随着hooks在React的出现以及组合式api在Vue3的出现,原本展示组件和容器组件之间清晰的边界变得模糊,Hooks和组合式API允许开发者能够封装和复用状态与逻辑而无需局限在在class组件或者OptionsAPI当中。
让我们开始创建1个应用,用于获取6个狗子的图片,并渲染在屏幕上
为了遵循容器/展示设计模式,我们将分离这个过程为两个部分,来强制分离关注点: 1.展示组件:关心怎么将数据展示给用户的组件。在这个例子当中,就是渲染狗狗的图片列表 2.容器组件:关心哪些数据展示给用户。在这个例子当中,就是获取狗的图像
获取狗的图像处理应用逻辑,而展示图片只处理视图
展示组件
展示组件通过props获取到它的data。它主要的功能就是简单地按我们的想法去展示它获得的数据,包括styles,但除了修改data
让我们看一下这个展示狗的例子。当渲染狗图片时,简单的映射每一张从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>
将这两个组件组合到一起,让从视图上分离应用程序的逻辑成为可能。
简单来说,这就是container/presentational模式。当集成了状态管理解决方案比如pinia,容器组件可以直接从store交互,fetch或mutating所需要的状态。这允许展示组件在应用程序的逻辑之外保持纯粹和无感知,只专注于它们所接收的props来渲染UI。
组合式函数
在很多的情况下,container/presentational 模式可以被组合式所代替。组合式的引入让开发者轻松地添加状态,而不需要一个容器组件来提供该状态。
除了将获取 data逻辑放在DogImagesContainer
组件当中,我们也可以创造一个组合式(相当于React的hooks)来fetch这些图片,然后返回dog的数组。
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,我们不再需要包裹DogImagesContainer
容器组件来获取数据然后发送到展示组件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
里修改该数据。
通过我们所做的所有变更,我们的app就像下面这个样子
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 };
}
组合式函数让在组件中分离视图和逻辑更加简单,就像是Container/Presentational设计模式一样。 它为我们节省了必须将展示组件包裹在容器组件当中的额外层。