发布于 2026-01-06 0 阅读
0

Vue 3 的组合式 API 和关注点分离

Vue 3 的组合式 API 和关注点分离

值得挖掘的新奇事物

我始终认为,最好将事物置于特定的语境中看待。因此,在 Vue 3.0 版本(目前为候选版本)发布之前,我首先撰写了一篇文章,阐述了我对 Vue 现状的看法。

然而,本系列文章的主要内容是 Vue 3 的一个新特性:组合式 API。这是我最期待的特性之一,现在终于到了我们讨论它的时候了!

崭新的玩具

这篇文章应该是本系列文章中最短的一篇,因为这个话题已经被比我有趣、聪明得多的人讨论过很多 次了。

Composition API 的创建是为了解决 Vue 应用程序规模扩大时出现的两个常见问题。

代码组织

你是否曾经维护过非常庞大的组件,它们逻辑复杂,包含大量的 `<Options>` datacomputed`<Options>`、methods`<Options>` 等?在阅读这类组件时,主要问题在于如何追踪每个组件的功能以及它们之间的交互方式。使用当前的 Options API,你需要在 Vue 实例内部来回切换,这会造成很大的认知负担。

Vue 3 尝试通过向 Vue 实例添加一个新方法 `.getElementById()` 来解决这个问题setup。这个方法可以看作是组件的入口点,它会在钩子函数之前被调用beforeCreated,并接收一个 `.getElementById props()` 参数。返回值是一个包含模板可用所有信息的对象

这里是编写所有组件逻辑的地方,无论我们说的是组件datacomputed组件、watcher组件等等。

这个难题仍然缺少一块拼图,我们如何在新方法中编写data、、以及更多computed内容watchermethodssetup

Vue 3提供了一个新工具来创建这些响应式数据等等:响应式 API

虽然现在可能不太相关,但这里有一个使用响应式 API 创建响应式数据的小示例:

import { ref } from 'vue';

const count = ref(0);

// Accessing ref's value in JS
console.log('Count:', count.value)

// Modifying the value
count.value += 1
Enter fullscreen mode Exit fullscreen mode

如你所见,在 JS 中操作 `'s` 时,必须显式地访问它ref的值。这让我有点不习惯,但在模板中则无需这样做,可以直接访问该值,我们稍后会看到。

请查阅响应式 API 的参考文档,了解更多相关信息。

好的,但是这两个 API 如何协同工作呢?让我们通过一个例子来说明。首先,我们将使用 Options API 来编写代码,然后我们将使用Vue 3 的方式来实现。

假设我们有一个组件用于管理博客文章的加载和显示,它的极简版本可能如下所示:

export default {
    name: 'blog-posts',
    data() {
        return {
            posts: [],
            loadingStatus: '',
            error: '',
        };
    },
    computed: {
        blogPostsLoading() {
            return this.loadingStatus === 'loading';
        },
        blogPostsLoadingError() {
            return this.loadingStatus === 'error';
        },
    },
    methods: {
        loadBlogPosts() {
            this.loadingStatus = 'loading';
            fetch(process.env.VUE_APP_POSTS_URL)
                .then((response) => {
                    if (!response.ok) {
                        throw new Error(response.status);
                    }
                    return reponse.json();
                })
                .then((posts) => {
                    this.posts = posts;
                    this.loadingStatus = 'loaded';
                })
                .catch((error) => {
                    this.error = error;
                    this.loadingStatus = 'error';
                });
        },
    },
    created() {
        this.loadBlogPosts();
    },
}
Enter fullscreen mode Exit fullscreen mode

利用新提供的工具,我们可以将所有逻辑放在setup

import { ref, computed } from 'vue';

export default {
    name: 'blog-posts',
    setup() {
        const loadingStatus = ref('');
        const error = ref('');
        const posts = ref([]);

        const blogPostsLoading = computed(() => {
            return loadingStatus.value === 'loading';
        });
        const blogPostsLoadingError = computed(() => {
            return loadingStatus.value === 'error';
        });

        const loadBlogPosts = () => {
            loadingStatus.value = 'loading';
            fetch(process.env.VUE_APP_POSTS_URL)
                .then((response) => {
                    if (!response.ok) {
                        throw new Error(response.status);
                    }
                    return reponse.json();
                })
                .then((fetchedPosts) => {
                    posts.value = fetchedPosts;
                    loadingStatus.value = 'loaded';
                })
                .catch((apiError) => {
                    error.value = apiError;
                    loadingStatus.value = 'error';
                });
        };

        // Return every information to be use by the template
        return {
            loadingStatus,
            // You can rename those information if needed
            loadingError: error,
            loadBlogPosts,
            blogPostsLoading,
            blogPostsLoadingError,
        };
    },
};
Enter fullscreen mode Exit fullscreen mode

在逻辑较少的组件中,它看起来可能不太实用,但它确实可以帮助开发者跟踪不同的组件,而无需在 Vue 实例的选项之间滚动。我们将在本文及后续文章中探讨如何充分利用它的功能。

我们还可以通过创建一个 ES 模块()来提取逻辑,posts.js该模块管理数据并公开有用的信息:

import { ref, computed } from 'vue';

export const useBlogPosts = () => {
    const loadingStatus = ref('');
    const error = ref('');
    const posts = ref([]);

    const blogPostsLoading = computed(() => {
        return loadingStatus.value === 'loading';
    });
    const blogPostsLoadingError = computed(() => {
        return loadingStatus.value === 'error';
    });

    const loadBlogPosts = () => {
        loadingStatus.value = 'loading';
        fetch(process.env.VUE_APP_POSTS_URL)
            .then((response) => {
                if (!response.ok) {
                    throw new Error(response.status);
                }
                return reponse.json();
            })
            .then((fetchedPosts) => {
                posts.value = fetchedPosts;
                loadingStatus.value = 'loaded';
            })
            .catch((apiError) => {
                error.value = apiError;
                loadingStatus.value = 'error';
            });
    };

    // Return every information to be use by the consumer (here, the template) 
    return {
        loadingStatus,
        // You can rename those information if needed
        loadingError: error,
        loadBlogPosts,
        blogPostsLoading,
        blogPostsLoadingError,
    };
}
Enter fullscreen mode Exit fullscreen mode

我们的组件将仅根据模块提供的数据来管理模板。完全的职责分离:

import { useBlogPosts } from './posts.js';

export default {
    name: 'blog-posts',
    setup() {
        const blogPostsInformation = useBlogPosts();
        return {
            loadingStatus: blogPostsInformation.loadingStatus,
            loadingError: blogPostsInformation.loadingError,
            loadBlogPosts: blogPostsInformation.loadBlogPosts,
            blogPostsLoading: blogPostsInformation.blogPostsLoading,
            blogPostsLoadingError: blogPostsInformation.blogPostsLoadingError,
        };
    },
};
Enter fullscreen mode Exit fullscreen mode

同样,这有助于理清代码,并将意图与实现解耦,这总是很好的做法。

您可能已经考虑过了,但这种创建模块的方式可以帮助我们重用逻辑!

逻辑重用

我们已经有一些工具可以帮助我们创建供多个组件使用的逻辑。例如,Mixins可以让你编写任何 Vue 实例的选项,并将其注入到一个或多个组件中。

这些方法都有一个共同的缺点,那就是缺乏清晰度。

除非通读所有 mixin,否则你永远无法清楚地知道哪个 mixin 导入了哪个选项。对于试图理解组件工作原理的开发者来说,这很容易变成一场噩梦,因为他们必须在 Vue 实例以及全局和局部注入的 mixin 之间来回切换。此外,mixin 的选项之间也可能相互冲突,导致一团糟,甚至可以说是混乱不堪。

借助组合式 API,任何组件都可以从不同的模块中选择所需的内容。setup方法中明确定义了内容的来源,开发者可以清楚地看到哪些内容来自哪里,甚至可以重命名变量以更好地理解其意图。

我认为,对于需要长期维护的应用程序而言,清晰度即使不是最重要的,也是最重要的。组合式 API 为我们提供了一种优雅而实用的工具来实现这一点。

等等,还有更多?

在我看来,这两个主要目标已经基本实现,但是 Composition API 不应该仅仅局限于这两个方面。

这也有利于我们应用程序的可测试性,让我解释一下原因。

在 Vue 3 之前,我们必须实例化组件,必要时注入模拟依赖项,然后才能开始编写断言。这种测试方式会导致测试套件与实际实现高度耦合,这种测试很快就会过时,而且弊大于利。

现在我们可以创建 ES 模块,封装领域逻辑并导出要使用的数据和方法。这些模块将几乎完全用 JavaScript 编写,因为它们仍然会使用 Vue 的 API,但并非在组件的上下文中。

我们的测试可以像我们的组件一样,直接使用导出的信息!

教学的艺术

你可能已经注意到,我正在撰写一系列关于这个新 API 的文章,可见我对它有多么兴奋。它解决了长期以来我一直想在前端应用程序中应用整洁代码原则的问题。我认为,如果使用得当,它将极大地帮助我们提高组件和应用程序的质量。

然而,组合式 API 是一个高级概念。我不认为它应该取代编写组件的实际方式。此外,我们仍然会遇到 Vue 3 之前编写的遗留代码,因此我们之前的知识仍然有用。

我在上一篇文章中已经讨论过这个问题,但记住这一点真的很重要:并非每个人都那么幸运,在几乎每天练习 Vue 两年后就能发现 3.0 版本。

有些人会从 Vue 3.0 版本开始使用,而像这样全新的 API 无疑会大大增加原本就很高的入门门槛。新手现在除了要掌握“传统”的 Options API 之外,还必须了解这个全新的 API。

您认为应该如何向新手介绍这个新的 API?我个人认为,它应该像 Vuex 或 Vue Router 一样,作为高级工具逐步引入。它应该建立在扎实的知识基础之上,并通过实践应用来提升使用体验。

欢迎大家再次分享自己的想法!

您对新的组合 API 有什么看法?

3.0版本发布后,你准备好立即使用了吗?

请把这件事告诉大家,我们一起来讨论吧 :)

既然理论部分已经介绍完毕,接下来该做什么呢?我将亲自动手实践,力求充分利用 Composition API,下一篇文章将从重构环节开始!

文章来源:https://dev.to/thomasferro/vue-3-s-composition-api-and-the-segregation-of-concerns-1k5c