各位靓仔靓女,大家好!我是今天的主讲人,江湖人称“代码老中医”,专治各种疑难杂症,尤其擅长用 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>
可以看到,虽然功能不多,但代码已经有点“挤”了。如果功能更多,代码会更加混乱,逻辑分散在不同的 data
、methods
、mounted
中,维护起来非常痛苦,就像在一堆乱麻中找线头。
而 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 的核心优势
-
逻辑组织性更强:
Composition API 可以将相关的逻辑组织在一起,形成一个个独立的组合函数,让代码更加清晰、易于理解和维护。就像把杂乱的房间整理成一个个整洁的抽屉,找东西方便多了。
-
代码复用性更高:
我们可以将组合函数提取出来,在不同的组件中复用,避免重复代码。就像把常用的工具放在一个工具箱里,需要的时候随时拿出来用。
-
类型推断更友好:
Composition API 结合 TypeScript 使用,可以更好地进行类型推断,减少错误。就像给工具箱里的每个工具都贴上标签,避免拿错。
-
更灵活的响应式系统:
Composition API 提供了
ref
、reactive
等响应式 API,可以更灵活地控制数据的响应性。就像可以根据需要调整水龙头的大小,控制水流。
四、如何优雅地使用 Composition API
-
善用组合函数(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
组合函数中,组件代码更加简洁。 -
合理使用
ref
和reactive
:ref
用于创建单个响应式变量,reactive
用于创建响应式对象。选择哪个取决于你的需求。一般来说,如果只需要追踪一个简单的数据类型,可以使用ref
;如果需要追踪一个复杂的数据结构,可以使用reactive
。import { ref, reactive } from 'vue'; // 使用 ref 创建一个响应式字符串 const name = ref('张三'); // 使用 reactive 创建一个响应式对象 const user = reactive({ name: '李四', age: 30, });
-
利用生命周期钩子:
Composition API 提供了
onMounted
、onUpdated
、onBeforeUnmount
等生命周期钩子,可以在组件的不同阶段执行相应的逻辑。import { onMounted, onBeforeUnmount } from 'vue'; onMounted(() => { // 组件挂载后执行 console.log('组件挂载了'); }); onBeforeUnmount(() => { // 组件卸载前执行 console.log('组件要卸载了'); });
-
结合 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 来实现,我们可以将这些功能分别封装成不同的组合函数:
-
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, }; }
-
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, }; }
-
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, }; }
-
商品列表组件:
<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 调理好自己的项目!如果大家还有什么问题,欢迎随时提问。祝大家编程愉快!