有兴趣阅读我们的下一本书吗?了解更多关于 使用 React 构建大型 JavaScript Web 应用程序

设计模式

可组合组件

选项 API

在 Vue 引入 Composition API 之前,开发人员依靠 **选项 API** 来组织组件逻辑,其中包括响应式数据、生命周期方法、计算属性等。选项 API 允许在特定选项中定义这些方面,如下面的示例所示

<!-- Template -->

<script>
  export default {
    name: "MyComponent",
    props: {
      // props
    },
    data() {
      // data
    },
    computed: {
      // computed properties
    },
    watch: {
      // properties to watch
    },
    methods: {
      // methods
    },
    created() {
      // lifecyle methods like created
    },
    // ...
  };
</script>

<!-- Styles -->

虽然这种方法已经达到了它的目的,并且在 Vue v3 中仍然适用,但随着组件越来越大、越来越复杂,它会变得难以管理和维护。在特定选项中定义组件逻辑会使代码更难阅读和理解,尤其是在处理大型组件时。在这个设置中,提取和重用组件之间的通用逻辑也可能很困难。

让我们看一个简单的 App 组件示例,它渲染两个单独的子组件——CountWidth

<template>
  <div class="App">
    <Count :count="count" :increment="increment" :decrement="decrement" />
    <div id="divider" />
    <Width :width="width" />
  </div>
</template>

<script>
  import Count from "./components/Count.vue";
  import Width from "./components/Width.vue";

  export default {
    name: "App",
    data() {
      return {
        count: 0,
        width: 0,
      };
    },
    mounted() {
      this.handleResize();
      window.addEventListener("resize", this.handleResize);
    },
    beforeUnmount() {
      window.removeEventListener("resize", this.handleResize);
    },
    methods: {
      increment() {
        this.count++;
      },
      decrement() {
        this.count--;
      },
      handleResize() {
        this.width = window.innerWidth;
      },
    },
    components: {
      Count,
      Width,
    },
  };
</script>

上面的代码片段表示一个名为 App 的 Vue 单文件组件 (SFC)。

<template> 部分定义了组件的标记。在本例中,它包含一个带有类“App”的 <div> 元素,它包装了两个子组件:<Count><Width>。这些子组件使用 Vue 的属性绑定语法 (:count:increment:decrement:width) 传递了某些属性。

<script> 部分包含组件的 JavaScript 代码。它首先从各自的文件导入 CountWidth 组件。export default 语句用于导出组件定义。在组件定义中,我们有

  • data 方法,它返回一个对象,其中包含组件的初始数据属性,即初始化为 0 的 countwidth
  • mounted() 生命周期钩子用于在组件已挂载到 DOM 中后执行代码。在本例中,它调用 handleResize() 方法并为 resize 事件添加事件监听器。
  • beforeUnmount() 生命周期钩子用于在组件卸载并销毁之前执行代码。在这里,它删除了 resize 事件的事件监听器。
  • methods 对象包含组件的方法。它定义了 increment()decrement()handleResize() 方法,这些方法基于某些事件或操作来操作 count 和 width 数据属性。
App.vue
1<template>
2 <div class="App">
3 <Count :count="count" :increment="increment" :decrement="decrement" />
4 <div id="divider" />
5 <Width :width="width" />
6 </div>
7 </template>
8
9 <script>
10 import Count from "./components/Count.vue";
11 import Width from "./components/Width.vue";
12
13 export default {
14 name: "App",
15 data() {
16 return {
17 count: 0,
18 width: 0,
19 };
20 },
21 mounted() {
22 this.handleResize();
23 window.addEventListener("resize", this.handleResize);
24 },
25 beforeUnmount() {
26 window.removeEventListener("resize", this.handleResize);
27 },
28 methods: {
29 increment() {
30 this.count++;
31 },
32 decrement() {
33 this.count--;
34 },
35 handleResize() {
36 this.width = window.innerWidth;
37 },
38 },
39 components: {
40 Count,
41 Width,
42 },
43 };
44 </script>

当应用程序运行时,当前计数和窗口的内部宽度会实时显示。用户可以通过使用 <Count> 组件中的按钮来增加和减少计数来与组件交互。

Incrementing and decrementing count

同样,每当窗口大小调整时,宽度会自动更新。

Increasing and decreasing window width

App.vue 单文件组件的结构可以可视化为以下内容

Flow chart

即使这个组件的大小很小,它内部的逻辑已经交织在一起。有些部分专门用于计数器的功能,而另一些部分则与宽度逻辑有关。随着组件的增长,组织和定位组件内的相关逻辑将变得更加困难。

为了解决这些挑战,Vue 团队在 Vue v3 中引入了 Composition API。

Composition API

Composition API 可以看作是 **提供代表 Vue 核心功能的独立函数的 API**。这些函数主要用在单个 setup() 选项中,该选项充当使用 Composition API 的入口点。

<!-- Template -->

<script>
  export default {
    name: "MyComponent",
    setup() {
      // the setup function
    },
  };
</script>

<!-- Styles -->

setup() 函数在组件创建之前以及组件的 props 可用时执行。

使用 Composition API,我们可以导入独立函数来帮助我们在组件中访问 Vue 的核心功能。让我们重写上面我们看到的计数器和宽度示例,同时依靠 Composition API 语法。

<template>
  <div class="App">
    <Count :count="count" :increment="increment" :decrement="decrement" />
    <div id="divider" />
    <Width :width="width" />
  </div>
</template>

<script>
  import { ref, onMounted, onBeforeUnmount } from "vue";
  import Count from "./components/Count.vue";
  import Width from "./components/Width.vue";

  export default {
    name: "App",
    setup() {
      const count = ref(0);
      const width = ref(0);

      const increment = () => {
        count.value++;
      };

      const decrement = () => {
        count.value--;
      };

      const handleResize = () => {
        width.value = window.innerWidth;
      };

      onMounted(() => {
        handleResize();
        window.addEventListener("resize", handleResize);
      });

      onBeforeUnmount(() => {
        window.removeEventListener("resize", handleResize);
      });

      return {
        count,
        width,
        increment,
        decrement,
      };
    },
    components: {
      Count,
      Width,
    },
  };
</script>

我们组件的 <template> 保持不变,但在组件的 <script> 部分,我们现在使用 setup() 函数使用 Composition API。

setup() 函数内部,我们

  • 使用 ref() 函数定义 countwidth 响应式变量——该函数接受单个基本值(例如字符串、数字等)并返回响应式/可变对象。
  • 我们还定义了自定义函数 increment()decrement()handleResize()。这些函数类似于我们在之前的选项 API 示例中定义的方法。
  • 我们使用 onMounted() 生命周期函数来调用自定义 handleResize() 函数,并在组件挂载时为 resize 事件添加事件监听器。同样,我们使用 onBeforeUnmount() 生命周期函数在组件卸载之前删除 resize 事件的事件监听器。
  • 然后返回在 setup() 函数中定义的响应式变量和函数,使它们可以在组件模板中访问。
App.vue
1<template>
2 <div class="App">
3 <Count :count="count" :increment="increment" :decrement="decrement" />
4 <div id="divider" />
5 <Width :width="width" />
6 </div>
7</template>
8
9<script>
10import { ref, onMounted, onBeforeUnmount } from "vue";
11import Count from "./components/Count.vue";
12import Width from "./components/Width.vue";
13
14export default {
15 name: "App",
16 setup() {
17 const count = ref(0);
18 const width = ref(0);
19
20 const increment = () => {
21 count.value++;
22 };
23
24 const decrement = () => {
25 count.value--;
26 };
27
28 const handleResize = () => {
29 width.value = window.innerWidth;
30 };
31
32 onMounted(() => {
33 handleResize();
34 window.addEventListener("resize", handleResize);
35 });
36
37 onBeforeUnmount(() => {
38 window.removeEventListener("resize", handleResize);
39 });
40
41 return {
42 count,
43 width,
44 increment,
45 decrement,
46 };
47 },
48 components: {
49 Count,
50 Width,
51 },
52};
53</script>

可组合组件

使用我们之前的代码示例,有人可能仍然想知道 setup() 函数如何为开发提供任何优势,因为它似乎只是要求我们将组件选项声明在一个函数中。

采用 Composition API 的一项重大优势是 **能够提取和重用组件之间共享的逻辑**。这是由以下事实驱动的:我们可以简单地声明自己的函数,这些函数使用 Vue 的全局可用组合函数,并使我们的函数 *能够轻松地在多个组件中使用以实现相同的结果*。

让我们进一步研究之前的计数器和宽度示例,创建一个可组合函数,它封装共享逻辑,可以在组件之间重用。

首先,让我们创建一个名为 useCounter 的可组合函数,这是一个封装计数器功能的可组合函数,并返回 count 的当前值、一个 increment() 方法和一个 decrement() 方法。

按照惯例,可组合函数名称以“use”关键字开头。

import { ref } from "vue";

export function useCounter(initialCount = 0) {
  const count = ref(initialCount);

  function increment() {
    count.value++;
  }

  function decrement() {
    count.value--;
  }

  return {
    count,
    increment,
    decrement,
  };
}

同样,我们可以创建一个名为 useWidth() 的可组合函数,它封装了应用程序的宽度功能。

import { ref, onMounted, onBeforeUnmount } from "vue";

export function useWidth() {
  const width = ref(0);

  function handleResize() {
    width.value = window.innerWidth;
  }

  onMounted(() => {
    handleResize();
    window.addEventListener("resize", handleResize);
  });

  onBeforeUnmount(() => {
    window.removeEventListener("resize", handleResize);
  });

  return {
    width,
  };
}

在我们的 App 组件中,我们现在可以使用可组合函数来实现相同的结果

<template>
  <div class="App">
    <Count :count="count" :increment="increment" :decrement="decrement" />
    <div id="divider" />
    <Width :width="width" />
  </div>
</template>

<script>
  import Count from "./components/Count.vue";
  import Width from "./components/Width.vue";
  import { useCounter } from "./composables/useCounter";
  import { useWidth } from "./composables/useWidth";

  export default {
    name: "App",
    components: {
      Count,
      Width,
    },
    setup() {
      const { count, increment, decrement } = useCounter(0);
      const { width } = useWidth();

      return {
        count,
        increment,
        decrement,
        width,
      };
    },
  };
</script>

通过这些更改,我们的应用程序将像以前一样运行,但在更可组合的设置中运行。

App.vue
1<template>
2 <div class="App">
3 <Count :count="count" :increment="increment" :decrement="decrement" />
4 <div id="divider" />
5 <Width :width="width" />
6 </div>
7</template>
8
9<script>
10import Count from "./components/Count.vue";
11import Width from "./components/Width.vue";
12import { useCounter } from "./composables/useCounter";
13import { useWidth } from "./composables/useWidth";
14
15export default {
16 name: "App",
17 components: {
18 Count,
19 Width,
20 },
21 setup() {
22 const { count, increment, decrement } = useCounter(0);
23 const { width } = useWidth();
24
25 return {
26 count,
27 increment,
28 decrement,
29 width,
30 };
31 },
32};
33</script>

通过在 Composition API 设置中使用可组合函数,我们能够将应用程序的上下文分解成更小的、可重用的部分,从而分离了逻辑。

让我们将我们刚刚进行的更改与最初的选项 API 示例组件进行比较,以可视化这些更改。

Flow chart

在 Vue 中使用可组合函数使我们能够将组件的逻辑分成几个更小的部分。重用相同的 stateful 逻辑现在变得容易,因为我们不再局限于在选项 API 中的特定选项中组织代码。

使用可组合函数,我们可以灵活地跨组件提取和重用共享逻辑。这种关注点分离使我们能够专注于每个可组合函数中的特定功能,从而使我们的代码 **更模块化、更易于维护**。

通过将逻辑分解成更小、可重用的部分,我们可以使用这些可组合函数来组合组件,将必要的功能整合在一起,而无需重复代码。这种方法促进了 **代码可重用性**,并降低了代码重复和不一致的风险。

此外,使用 Composition API 提高了组件逻辑的 **可读性** 和 **可理解性**。每个可组合函数都封装了组件行为的特定方面,使推理和测试变得更容易。它还允许团队成员之间更容易协作,因为代码变得更有条理和组织性。

最后,使用 Composition API 构建 Vue 应用程序可以实现 **更好的类型推断**。由于 Composition API 帮助我们使用变量和标准 JavaScript 函数来处理组件逻辑,因此使用 TypeScript 等静态类型系统来构建大型 Vue 应用程序变得容易得多!

有用资源