[译]设计模式系列之容器/展示模式(vue篇)

195 阅读5分钟

译者前言

日常维护当中少不了一些技改需求,比如说重构xx,或者干脆开发的时候就留足扩展性,可维护性

项目内写屎山多吧? 如何避免屎山,可以学习一些设计模式,让代码更容易维护和扩展,但不利于防御性编程😀。 Container/Presentational Pattern (patterns.dev)

正文:

在2015年,Dan Abramov写了一篇名为“展示组件与容器组件”的文章,这改变了开发者对React当中组件结构的看法。他介绍了一种设计模式,能够将组件分为两种类型:

  1. 展示组件(或者说是哑巴组件)它们关心事物长啥样。不细究数据是如何装载和变化的,只通过props接受数据和回调函数
  2. 容器组件(或者说是聪明组件)它们只关心事物是如何工作的。它们提供数据和行为给展示或其他容器组件

此设计模式主要和React相关联,但它的基本原则被采纳和应用在不同种类的框架和库当中。

Dan与众不同的提议为构建javascript应用程序提供了1个更清晰、更可拓展的方式。 通过清晰的定义不同类型组件的职责,开发者可以确保UI组件(展示)和逻辑(容器组件)有更好的复用性。这个想法就是:如果我们改变某些东西的样子(比如按钮的设计),无需改动到这个App的逻辑,相反地,如果我们需要改变数据的流向或者处理,展示组件不会被改动,确保UI始终如一。

然而,伴随着hooks在React的出现以及组合式api在Vue3的出现,原本展示组件和容器组件之间清晰的边界变得模糊,Hooks和组合式API允许开发者能够封装和复用状态与逻辑而无需局限在在class组件或者OptionsAPI当中。

让我们开始创建1个应用,用于获取6个狗子的图片,并渲染在屏幕上 image.png

为了遵循容器/展示设计模式,我们将分离这个过程为两个部分,来强制分离关注点: 1.展示组件:关心怎么将数据展示给用户的组件。在这个例子当中,就是渲染狗狗的图片列表 2.容器组件:关心哪些数据展示给用户。在这个例子当中,就是获取狗的图像

获取狗的图像处理应用逻辑,而展示图片只处理视图

image.png

展示组件

展示组件通过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里修改该数据。

image.png

通过我们所做的所有变更,我们的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设计模式一样。 它为我们节省了必须将展示组件包裹在容器组件当中的额外层。