探讨 Vue 3 中 Composition API 在大型项目中的应用,以及它如何提升代码可读性、可维护性性和逻辑复用。

各位靓仔靓女,大家好!我是今天的主讲人,江湖人称“代码老中医”,专治各种疑难杂症,尤其擅长用 Vue 3 的 Composition API 调理大型项目,让它从头到脚焕然一新,变得可读性强、可维护性高、还能实现代码复用。今天就跟大家唠唠嗑,聊聊 Composition API 在大型项目里的那些事儿。

咱们先来想想,以前 Vue 2 的 Options API 就像一个装修好的房子,客厅是 data,卧室是 methods,厨房是 computed,阳台是 watch。虽然井井有条,但如果想把厨房里的炒菜锅搬到卧室里用,就有点麻烦,得跨房间操作,代码耦合度高。

而 Composition API 就像一个毛坯房,你想怎么设计就怎么设计,厨房、卧室、客厅随你安排,只要你高兴,把炒菜锅搬到卧室里也不是不行,灵活性大大提高。

一、为什么要拥抱 Composition API?

大型项目嘛,代码量肯定巨大,组件也多如繁星。Options API 在这种情况下,很容易让代码变得臃肿不堪,就像一个塞满了东西的仓库,找个东西得翻箱倒柜。

举个例子,假设我们有个组件,需要实现以下几个功能:

  • 记录鼠标位置
  • 监听窗口大小变化
  • 提供一个定时器,每秒更新时间

用 Options API 来实现,代码可能会是这样:

<template>
  <div>
    <p>鼠标位置:X: {{ mouseX }}, Y: {{ mouseY }}</p>
    <p>窗口大小:Width: {{ windowWidth }}, Height: {{ windowHeight }}</p>
    <p>当前时间:{{ currentTime }}</p>
  </div>
</template>

<script>
export default {
  data() {
    return {
      mouseX: 0,
      mouseY: 0,
      windowWidth: 0,
      windowHeight: 0,
      currentTime: new Date().toLocaleTimeString(),
      timer: null,
    };
  },
  mounted() {
    this.handleMouseMove();
    this.handleResize();
    this.startTimer();
    window.addEventListener('mousemove', this.handleMouseMove);
    window.addEventListener('resize', this.handleResize);
  },
  beforeUnmount() {
    window.removeEventListener('mousemove', this.handleMouseMove);
    window.removeEventListener('resize', this.handleResize);
    this.stopTimer();
  },
  methods: {
    handleMouseMove(event) {
      this.mouseX = event.clientX;
      this.mouseY = event.clientY;
    },
    handleResize() {
      this.windowWidth = window.innerWidth;
      this.windowHeight = window.innerHeight;
    },
    startTimer() {
      this.timer = setInterval(() => {
        this.currentTime = new Date().toLocaleTimeString();
      }, 1000);
    },
    stopTimer() {
      clearInterval(this.timer);
      this.timer = null;
    },
  },
};
</script>

可以看到,虽然功能不多,但代码已经有点“挤”了。如果功能更多,代码会更加混乱,逻辑分散在不同的 datamethodsmounted 中,维护起来非常痛苦,就像在一堆乱麻中找线头。

而 Composition API 可以很好地解决这个问题,它可以将相关的逻辑组织在一起,形成一个个独立的“组合函数”(Composition Functions),让代码更加清晰、易于理解和复用。

二、Composition API 的基本用法

Composition API 主要通过 setup 函数来组织代码。setup 函数是一个新的组件选项,它会在组件创建之前执行,可以返回一个对象,对象中的属性和方法可以在模板中使用。

用 Composition API 重写上面的例子:

<template>
  <div>
    <p>鼠标位置:X: {{ mouseX }}, Y: {{ mouseY }}</p>
    <p>窗口大小:Width: {{ windowWidth }}, Height: {{ windowHeight }}</p>
    <p>当前时间:{{ currentTime }}</p>
  </div>
</template>

<script>
import { ref, onMounted, onBeforeUnmount } from 'vue';

export default {
  setup() {
    // 鼠标位置逻辑
    const mouseX = ref(0);
    const mouseY = ref(0);

    const handleMouseMove = (event) => {
      mouseX.value = event.clientX;
      mouseY.value = event.clientY;
    };

    onMounted(() => {
      window.addEventListener('mousemove', handleMouseMove);
    });

    onBeforeUnmount(() => {
      window.removeEventListener('mousemove', handleMouseMove);
    });

    // 窗口大小逻辑
    const windowWidth = ref(0);
    const windowHeight = ref(0);

    const handleResize = () => {
      windowWidth.value = window.innerWidth;
      windowHeight.value = window.innerHeight;
    };

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

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

    // 定时器逻辑
    const currentTime = ref(new Date().toLocaleTimeString());
    let timer = null;

    const startTimer = () => {
      timer = setInterval(() => {
        currentTime.value = new Date().toLocaleTimeString();
      }, 1000);
    };

    const stopTimer = () => {
      clearInterval(timer);
      timer = null;
    };

    onMounted(() => {
      startTimer();
    });

    onBeforeUnmount(() => {
      stopTimer();
    });

    return {
      mouseX,
      mouseY,
      windowWidth,
      windowHeight,
      currentTime,
    };
  },
};
</script>

虽然代码量看起来差不多,但逻辑更加清晰了。我们可以看到,鼠标位置、窗口大小和定时器这三个功能的代码被分别组织在一起,易于理解和维护。

三、Composition API 的核心优势

  1. 逻辑组织性更强:

    Composition API 可以将相关的逻辑组织在一起,形成一个个独立的组合函数,让代码更加清晰、易于理解和维护。就像把杂乱的房间整理成一个个整洁的抽屉,找东西方便多了。

  2. 代码复用性更高:

    我们可以将组合函数提取出来,在不同的组件中复用,避免重复代码。就像把常用的工具放在一个工具箱里,需要的时候随时拿出来用。

  3. 类型推断更友好:

    Composition API 结合 TypeScript 使用,可以更好地进行类型推断,减少错误。就像给工具箱里的每个工具都贴上标签,避免拿错。

  4. 更灵活的响应式系统:

    Composition API 提供了 refreactive 等响应式 API,可以更灵活地控制数据的响应性。就像可以根据需要调整水龙头的大小,控制水流。

四、如何优雅地使用 Composition API

  1. 善用组合函数(Composition Functions):

    将相关的逻辑封装成独立的组合函数,可以提高代码的可读性和复用性。例如,我们可以创建一个 useMouse 组合函数来处理鼠标位置相关的逻辑:

    import { ref, onMounted, onBeforeUnmount } from 'vue';
    
    export function useMouse() {
      const mouseX = ref(0);
      const mouseY = ref(0);
    
      const handleMouseMove = (event) => {
        mouseX.value = event.clientX;
        mouseY.value = event.clientY;
      };
    
      onMounted(() => {
        window.addEventListener('mousemove', handleMouseMove);
      });
    
      onBeforeUnmount(() => {
        window.removeEventListener('mousemove', handleMouseMove);
      });
    
      return {
        mouseX,
        mouseY,
      };
    }

    然后在组件中使用:

    <template>
      <div>
        <p>鼠标位置:X: {{ mouseX }}, Y: {{ mouseY }}</p>
      </div>
    </template>
    
    <script>
    import { useMouse } from './useMouse';
    
    export default {
      setup() {
        const { mouseX, mouseY } = useMouse();
    
        return {
          mouseX,
          mouseY,
        };
      },
    };
    </script>

    这样,鼠标位置相关的逻辑就被封装到了 useMouse 组合函数中,组件代码更加简洁。

  2. 合理使用 refreactive

    ref 用于创建单个响应式变量,reactive 用于创建响应式对象。选择哪个取决于你的需求。一般来说,如果只需要追踪一个简单的数据类型,可以使用 ref;如果需要追踪一个复杂的数据结构,可以使用 reactive

    import { ref, reactive } from 'vue';
    
    // 使用 ref 创建一个响应式字符串
    const name = ref('张三');
    
    // 使用 reactive 创建一个响应式对象
    const user = reactive({
      name: '李四',
      age: 30,
    });
  3. 利用生命周期钩子:

    Composition API 提供了 onMountedonUpdatedonBeforeUnmount 等生命周期钩子,可以在组件的不同阶段执行相应的逻辑。

    import { onMounted, onBeforeUnmount } from 'vue';
    
    onMounted(() => {
      // 组件挂载后执行
      console.log('组件挂载了');
    });
    
    onBeforeUnmount(() => {
      // 组件卸载前执行
      console.log('组件要卸载了');
    });
  4. 结合 TypeScript 使用:

    TypeScript 可以帮助我们更好地进行类型推断,减少错误。

    import { ref, defineComponent } from 'vue';
    
    export default defineComponent({
      setup() {
        const count = ref<number>(0); // 声明 count 为 number 类型
    
        const increment = () => {
          count.value++;
        };
    
        return {
          count,
          increment,
        };
      },
    });

五、Composition API 在大型项目中的实战案例

假设我们正在开发一个电商网站,需要实现一个商品列表组件,该组件需要实现以下功能:

  • 从服务器获取商品列表
  • 支持分页
  • 支持搜索
  • 支持按价格排序

用 Composition API 来实现,我们可以将这些功能分别封装成不同的组合函数:

  1. useProducts:获取商品列表

    import { ref, onMounted } from 'vue';
    import axios from 'axios'; // 引入 axios
    
    export function useProducts(page = 1, search = '', sortBy = 'price') {
      const products = ref([]);
      const loading = ref(false);
      const error = ref(null);
      const total = ref(0);
    
      const fetchProducts = async () => {
        loading.value = true;
        error.value = null;
    
        try {
          const response = await axios.get('/api/products', { // 假设 API 接口为 /api/products
            params: {
              page,
              search,
              sortBy,
            },
          });
          products.value = response.data.data; // 假设接口返回的数据结构为 { data: [], total: number }
          total.value = response.data.total;
        } catch (err) {
          error.value = err;
        } finally {
          loading.value = false;
        }
      };
    
      onMounted(fetchProducts);
    
      return {
        products,
        loading,
        error,
        total,
        fetchProducts,
      };
    }
  2. usePagination:处理分页逻辑

    import { ref } from 'vue';
    
    export function usePagination(total, pageSize = 10) {
      const currentPage = ref(1);
      const totalPages = ref(Math.ceil(total / pageSize));
    
      const goToPage = (page) => {
        currentPage.value = page;
      };
    
      return {
        currentPage,
        totalPages,
        goToPage,
      };
    }
  3. useSearch:处理搜索逻辑

    import { ref, watch } from 'vue';
    
    export function useSearch(initialSearch = '') {
      const search = ref(initialSearch);
      const debouncedSearch = ref(initialSearch); // 用于防抖
    
      let timeoutId = null;
    
      watch(search, (newSearch) => {
        clearTimeout(timeoutId);
        timeoutId = setTimeout(() => {
          debouncedSearch.value = newSearch;
        }, 300); // 300ms 防抖
      });
    
      return {
        search,
        debouncedSearch,
      };
    }
  4. 商品列表组件:

    <template>
      <div>
        <input type="text" v-model="search" placeholder="搜索商品" />
        <button @click="fetchProducts">搜索</button>
    
        <ul>
          <li v-for="product in products" :key="product.id">
            {{ product.name }} - {{ product.price }}
          </li>
        </ul>
    
        <div v-if="loading">加载中...</div>
        <div v-if="error">出错啦:{{ error.message }}</div>
    
        <div>
          <button :disabled="currentPage === 1" @click="goToPage(currentPage - 1)">上一页</button>
          <span>{{ currentPage }} / {{ totalPages }}</span>
          <button :disabled="currentPage === totalPages" @click="goToPage(currentPage + 1)">下一页</button>
        </div>
      </div>
    </template>
    
    <script>
    import { useProducts } from './useProducts';
    import { usePagination } from './usePagination';
    import { useSearch } from './useSearch';
    import { watch } from 'vue';
    
    export default {
      setup() {
        const { search, debouncedSearch } = useSearch();
        const { products, loading, error, total, fetchProducts } = useProducts();
        const { currentPage, totalPages, goToPage } = usePagination(total.value);
    
        // 监听搜索词变化,重新获取商品列表
        watch(debouncedSearch, () => {
          fetchProducts(currentPage.value, debouncedSearch.value);
        });
    
        // 监听页码变化,重新获取商品列表
        watch(currentPage, () => {
          fetchProducts(currentPage.value, debouncedSearch.value);
        });
    
        return {
          products,
          loading,
          error,
          currentPage,
          totalPages,
          goToPage,
          search,
          fetchProducts,
        };
      },
    };
    </script>

    在这个例子中,我们将商品列表、分页和搜索的逻辑分别封装成了独立的组合函数,然后在商品列表组件中组合使用。这样,代码更加清晰、易于理解和维护。而且,这些组合函数可以在其他组件中复用,例如,我们可以创建一个商品分类列表组件,也需要获取商品列表,就可以直接使用 useProducts 组合函数。

六、Options API 和 Composition API 如何选择?

特性 Options API Composition API
代码组织 基于选项 (data, methods, computed, watch) 基于函数 (组合函数)
代码复用 Mixins (容易命名冲突) 组合函数 (逻辑清晰,避免命名冲突)
可读性 小规模项目较好,大规模项目较差 大规模项目更清晰,逻辑更内聚
类型推断 较弱 结合 TypeScript 更强
适用场景 小型、简单的项目 大型、复杂的项目,需要高复用性和可维护性

总的来说,Options API 更适合小型、简单的项目,而 Composition API 更适合大型、复杂的项目。当然,这并不是绝对的,你可以根据自己的实际情况选择合适的 API。

七、总结

Composition API 是 Vue 3 带来的一个重要的改进,它可以帮助我们更好地组织代码、提高代码的复用性和可维护性。尤其是在大型项目中,Composition API 的优势更加明显。

虽然 Composition API 刚开始学习起来可能会有点陌生,但只要掌握了基本概念和用法,就能感受到它的强大之处。希望今天的分享能帮助大家更好地理解和使用 Composition API,让你的 Vue 项目更加健壮、易于维护。

好了,今天的讲座就到这里,希望大家都能成为代码界的“老中医”,用 Composition API 调理好自己的项目!如果大家还有什么问题,欢迎随时提问。祝大家编程愉快!

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注