Vue 中使用 Composition API 实现逻辑抽离和复用

314 阅读5分钟

在开发过程中,我们经常会遇到组件逻辑变得复杂、难以维护的问题。比如说,我在开发一个博客系统时,需要一个组件来显示博客文章列表,并且支持文章的搜索和分页功能。如果我把所有逻辑都放在一个组件中,会导致代码难以管理和复用。为了解决这个问题,Vue 提供了 Composition API,它可以帮助我将组件的逻辑抽离到独立的函数中,提升代码的可读性和可维护性。

场景介绍

我需要实现一个博客文章列表组件,包括以下功能:

  1. 获取博客文章数据
  2. 实现分页
  3. 实现搜索功能

为什么要进行逻辑抽离和复用

代码的可读性和可维护性

假设你把所有逻辑都写在一个组件里,随着功能的增加,这个组件会变得非常臃肿。各种数据、方法、计算属性和生命周期钩子混在一起,代码很快就会变得难以阅读和维护。比如说,组件内部会充斥着各种数据定义、方法实现、复杂的计算属性和众多的生命周期钩子,代码看起来会像这样:

<template>
  <div>
    <input v-model="query" placeholder="Search for posts..." @input="handleSearch" />
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
    <button @click="prevPage" :disabled="currentPage === 1">Previous</button>
    <button @click="nextPage" :disabled="currentPage === totalPages">Next</button>
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
  </div>
</template>
​
<script>
export default {
  data() {
    return {
      posts: [],
      loading: true,
      error: null,
      query: '',
      currentPage: 1,
      totalPages: 0,
    };
  },
  computed: {
    filteredPosts() {
      return this.posts.filter(post => post.title.includes(this.query));
    }
  },
  methods: {
    async fetchPosts(query = '', page = 1) {
      this.loading = true;
      try {
        const response = await fetch(`https://api.example.com/posts?query=${query}&page=${page}`);
        const data = await response.json();
        this.posts = data.posts;
        this.totalPages = data.totalPages;
        this.loading = false;
      } catch (err) {
        this.error = err;
        this.loading = false;
      }
    },
    handleSearch() {
      this.currentPage = 1;
      this.fetchPosts(this.query, this.currentPage);
    },
    prevPage() {
      if (this.currentPage > 1) {
        this.currentPage--;
        this.fetchPosts(this.query, this.currentPage);
      }
    },
    nextPage() {
      if (this.currentPage < this总页数) {
        this.currentPage++;
        this.fetchPosts(this.query, this.currentPage);
      }
    },
  },
  watch: {
    query: 'fetchPosts',
    currentPage: 'fetchPosts',
  },
  created() {
    this.fetchPosts();
  }
};
</script>

看起来是不是有点头疼?所有的逻辑都混在一起,当你需要修改或添加新功能时,很难快速找到相关的代码部分,而且一不小心就可能引入新的 bug。

逻辑复用

在大型项目中,相同的逻辑可能会在多个组件中重复出现。比如说,数据获取、分页和搜索逻辑。如果不进行抽离和复用,会导致大量的重复代码,增加维护成本。通过 Composition API,可以将这些逻辑封装成独立的函数,在不同的组件中轻松复用,减少重复代码。

灵活性和扩展性

通过抽离逻辑,我们可以为每个独立的逻辑函数添加参数,使其更加灵活。比如,数据获取函数可以接收不同的 API 地址,以适应不同的数据源需求。这样设计的代码更加模块化和可扩展,需求变化时,只需调整参数即可,无需修改核心逻辑。

更好的测试和调试

将复杂的逻辑拆分成独立的函数,可以更方便地进行单元测试。每个函数只关注单一逻辑,使测试变得更简单。调试时,我们也可以更容易地定位问题所在,提高开发效率。

实现逻辑抽离和复用

下面是我在开发博客系统时,如何使用 Composition API 将不同的逻辑抽离和复用的示例。

数据获取逻辑的抽离

首先,我需要一个函数来获取博客文章的数据。我可以使用 ref 来创建响应式的数据,并使用 onMounted 来在组件挂载时获取数据。

// useFetchPosts.js
import { ref, onMounted } from 'vue';
​
export function useFetchPosts(apiUrl) {
  const posts = ref([]);
  const loading = ref(true);
  const error = ref(null);
​
  const fetchPosts = async (query = '', page = 1) => {
    loading.value = true;
    try {
      const response = await fetch(`${apiUrl}?query=${query}&page=${page}`);
      const data = await response.json();
      posts.value = data.posts;
      loading.value = false;
    } catch (err) {
      error.value = err;
      loading.value = false;
    }
  };
​
  onMounted(() => {
    fetchPosts();
  });
​
  return {
    posts,
    loading,
    error,
    fetchPosts,
  };
}

这样,我把数据获取的逻辑抽离到了一个独立的函数 useFetchPosts 中,这个函数接收一个 apiUrl 参数,可以灵活地用于不同的 API 请求。

分页逻辑的抽离

接下来,我需要一个用于管理分页的逻辑。我可以创建一个 usePagination 函数,里面包含当前页数和总页数的状态管理。

// usePagination.js
import { ref } from 'vue';
​
export function usePagination() {
  const currentPage = ref(1);
  const totalPages = ref(0);
​
  const setPage = (page) => {
    currentPage.value = page;
  };
​
  return {
    currentPage,
    totalPages,
    setPage,
  };
}

这样,我把分页的逻辑独立出来,使得它可以在多个组件中复用。

搜索逻辑的抽离

我还需要一个函数来管理搜索查询。这同样可以通过创建一个 useSearch 函数来实现。

// useSearch.js
import { ref } from 'vue';
​
export function useSearch() {
  const query = ref('');
​
  const setQuery = (newQuery) => {
    query.value = newQuery;
  };
​
  return {
    query,
    setQuery,
  };
}

这个 useSearch 函数将搜索查询的状态和更新逻辑封装在一起,便于在多个组件中复用。

组合使用这些逻辑

最后,我可以在组件中组合使用这些独立的逻辑函数。这样不仅使得代码更加清晰,还能提高复用性。

<template>
  <div>
    <input v-model="query" placeholder="Search for posts..." @input="handleSearch" />
    <ul>
      <li v-for="post in posts" :key="post.id">{{ post.title }}</li>
    </ul>
    <button @click="prevPage" :disabled="currentPage === 1">Previous</button>
    <button @click="nextPage" :disabled="currentPage === totalPages">Next</button>
    <p v-if="loading">Loading...</p>
    <p v-if="error">Error: {{ error.message }}</p>
  </div>
</template><script>
import { useFetchPosts } from './useFetchPosts';
import { usePagination } from './usePagination';
import { useSearch } from './useSearch';
import { watch } from 'vue';
​
export default {
  setup() {
    const { posts, loading, error, fetchPosts } = useFetchPosts('https://api.example.com/posts');
    const { currentPage, setPage, totalPages } = usePagination();
    const { query, setQuery } = useSearch();
​
    const handleSearch = () => {
      fetchPosts(query.value, currentPage.value);
    };
​
    const prevPage = () => {
      if (currentPage.value > 1) {
        setPage(currentPage.value - 1);
        fetchPosts(query.value, currentPage.value);
      }
    };
​
    const nextPage = () => {
      setPage(currentPage.value + 1);
      fetchPosts(query.value, currentPage.value);
    };
​
    watch([query, currentPage], () => {
      fetchPosts(query.value, currentPage.value);
    });
​
    return {
      posts,
      loading,
      error,
      query,
      handleSearch,
      currentPage,
      totalPages,
      prevPage,
      nextPage,
    };
  },
};
</script>

总结

通过把逻辑拆分到独立的函数中,我发现代码变得更清晰、更容易维护。以前那种所有东西都堆在一个组件里的情况,现在可以避免了。无论是数据获取、分页,还是搜索功能,每个功能都有自己专门的函数,这样不但减少了重复代码,还让整个项目更灵活、更易扩展。如果以后需要增加新功能或者调整现有功能,只需要修改对应的函数,而不用担心会影响到其他部分。这种方式不仅让开发过程更高效,还让调试和测试变得更加简单。