𝑻𝒆𝒏𝑪𝒍𝒂𝒘正在头脑风暴···
𝑻𝒆𝒏𝑲𝒊𝑺𝒆𝒀𝒂の𝑨𝒈𝒆𝒏𝒕助手
𝑻𝒆𝒏-𝒇𝒍𝒂𝒔𝒉

Vue 3 Composition API实战 - 可复用组件开发技巧

作为一个Vue开发者,从Vue 2升级到Vue 3之后,Composition API给我带来了全新的编程体验。说实话,刚开始接触Composition API的时候,我也有点不习惯,毕竟Options API已经用了很多年。但经过几个项目的实践,我发现Composition API在代码组织和复用方面确实有着天然的优势。

今天我就来和大家分享一下我在实际项目中使用Vue 3 Composition API的一些经验和技巧,希望能对大家有所帮助。

一、初识Composition API

Composition API是Vue 3引入的一大新特性,它让我们能够使用函数的方式组织和复用组件逻辑。相比于Options API的分散式结构,Composition API允许我们将相关的逻辑组织在一起。

// Options API - 逻辑分散
export default {
data() {
return {
count: 0,
loading: false,
error: null,
items: []
}
},
computed: {
doubleCount() {
return this.count * 2;
}
},
methods: {
increment() {
this.count++;
},
async fetchData() {
this.loading = true;
try {
const response = await api.get('/items');
this.items = response.data;
} catch (error) {
this.error = error;
} finally {
this.loading = false;
}
}
},
mounted() {
this.fetchData();
}
}
// Composition API - 逻辑集中
import { ref, computed, onMounted } from 'vue';

export function useCounter() {
const count = ref(0);
const doubleCount = computed(() => count.value * 2);

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

return { count, doubleCount, increment };
}

export function useFetch(url) {
const data = ref([]);
const loading = ref(false);
const error = ref(null);

const fetchData = async () => {
loading.value = true;
try {
const response = await fetch(url);
data.value = await response.json();
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};

onMounted(fetchData);

return { data, loading, error, fetchData };
}

二、组合式函数 - 代码复用的核心

组合式函数(Composables)是Composition API的精髓所在,它让我们能够将可复用的逻辑抽取成独立的函数。

1. 基础组合式函数

// useLocalStorage.js
import { ref, watch } from 'vue';

export function useLocalStorage(key, defaultValue) {
const storedValue = ref(localStorage.getItem(key));

const setValue = (value) => {
localStorage.setItem(key, value);
storedValue.value = value;
};

if (storedValue.value === null) {
setValue(defaultValue);
}

// 监听存储变化
window.addEventListener('storage', (event) => {
if (event.key === key) {
storedValue.value = event.newValue;
}
});

return [storedValue, setValue];
}

使用方式:

import { useLocalStorage } from './composables/useLocalStorage';

export default {
setup() {
const [theme, setTheme] = useLocalStorage('theme', 'light');

return { theme, setTheme };
}
}

2. 复杂的组合式函数

// useAsyncData.js
import { ref, watchEffect } from 'vue';

export function useAsyncData(apiFn, options = {}) {
const data = ref(null);
const loading = ref(false);
const error = ref(null);

const execute = async (params) => {
loading.value = true;
error.value = null;

try {
const result = await apiFn(params);
data.value = result;
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};

// 自动执行
if (options.auto !== false) {
watchEffect(() => {
execute(options.params);
});
}

return { data, loading, error, execute };
}

使用方式:

import { useAsyncData } from './composables/useAsyncData';
import axios from 'axios';

export default {
setup() {
const { data, loading, error, execute } = useAsyncData(
(params) => axios.get('/api/users', { params }),
{ auto: true, params: { page: 1 } }
);

return { data, loading, error, execute };
}
}

3. 表单处理组合式函数

// useForm.js
import { ref, reactive } from 'vue';

export function useForm(initialValues, validationRules) {
const form = reactive({ ...initialValues });
const errors = reactive({});
const touched = reactive({});

const validateField = (field) => {
if (!validationRules[field]) return true;

const rule = validationRules[field];
const value = form[field];

if (rule.required && !value) {
errors[field] = rule.message || '此字段为必填项';
return false;
}

if (rule.pattern && !rule.pattern.test(value)) {
errors[field] = rule.message || '格式不正确';
return false;
}

delete errors[field];
return true;
};

const validateForm = () => {
let isValid = true;

Object.keys(validationRules).forEach(field => {
if (!validateField(field)) {
isValid = false;
}
touched[field] = true;
});

return isValid;
};

const setFieldValue = (field, value) => {
form[field] = value;
touched[field] = true;
validateField(field);
};

const resetForm = () => {
Object.keys(form).forEach(field => {
form[field] = initialValues[field];
errors[field] = undefined;
touched[field] = false;
});
};

return {
form,
errors,
touched,
validateField,
validateForm,
setFieldValue,
resetForm
};
}

使用方式:

import { useForm } from './composables/useForm';

export default {
setup() {
const { form, errors, touched, validateForm, setFieldValue, resetForm } = useForm(
{
username: '',
email: '',
password: ''
},
{
username: {
required: true,
pattern: /^[a-zA-Z0-9_]{4,20}$/,
message: '用户名必须是4-20位的字母数字下划线'
},
email: {
required: true,
pattern: /^[^\s@]+@[^\s@]+\.[^\s@]+$/,
message: '请输入有效的邮箱地址'
},
password: {
required: true,
pattern: /^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,}$/,
message: '密码至少8位,必须包含字母和数字'
}
}
);

const handleSubmit = () => {
if (validateForm()) {
// 提交表单
console.log('Form submitted:', form);
}
};

return { form, errors, touched, setFieldValue, handleSubmit, resetForm };
}
}

4. 搜索和分页组合式函数

// usePaginatedData.js
import { ref, watchEffect } from 'vue';

export function usePaginatedData(fetchFn, options = {}) {
const data = ref([]);
const loading = ref(false);
const error = ref(null);
const currentPage = ref(1);
const totalItems = ref(0);
const totalPages = ref(0);

const pagination = computed(() => ({
current: currentPage.value,
total: totalPages.value,
totalItems: totalItems.value,
hasNext: currentPage.value < totalPages.value,
hasPrev: currentPage.value > 1
}));

const loadData = async (page = currentPage.value, params = {}) => {
loading.value = true;
error.value = null;

try {
const response = await fetchFn(page, params);
data.value = response.data;
totalItems.value = response.total;
totalPages.value = Math.ceil(response.total / options.pageSize || 10);
} catch (err) {
error.value = err;
} finally {
loading.value = false;
}
};

const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++;
loadData();
}
};

const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--;
loadData();
}
};

const goToPage = (page) => {
currentPage.value = Math.max(1, Math.min(page, totalPages.value));
loadData();
};

// 自动加载数据
watchEffect(() => {
loadData();
});

return {
data,
loading,
error,
currentPage,
totalItems,
totalPages,
pagination,
loadData,
nextPage,
prevPage,
goToPage
};
}

使用方式:

import { usePaginatedData } from './composables/usePaginatedData';

export default {
setup() {
const { data, loading, error, pagination, nextPage, prevPage } = usePaginatedData(
(page) => api.get('/api/products', { page, limit: 10 }),
{ pageSize: 10 }
);

return { data, loading, error, pagination, nextPage, prevPage };
}
}

三、组合式函数的最佳实践

1. 单一职责原则

每个组合式函数应该专注于单一的功能,这样更容易测试和维护。

// 好的做法:功能单一
import { useLocalStorage } from './useLocalStorage';
import { useTheme } from './useTheme';

export function useAppSettings() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const darkMode = computed(() => theme.value === 'dark');

const toggleTheme = () => {
const newTheme = darkMode.value ? 'light' : 'dark';
setTheme(newTheme);
};

return { theme, darkMode, toggleTheme };
}

// 避免:功能混杂
export function useAppSettingsBad() {
const [theme, setTheme] = useLocalStorage('theme', 'light');
const darkMode = computed(() => theme.value === 'dark');
const [fontSize, setFontSize] = useLocalStorage('fontSize', 'medium');
const [language, setLanguage] = useLocalStorage('language', 'zh-CN');

// 各种功能混在一起,难以维护
}

2. 类型安全

如果使用TypeScript,可以为组合式函数添加类型定义:

// types.ts
export interface FormField {
value: string;
touched: boolean;
error?: string;
}

export interface FormOptions {
initialValues: Record<string, any>;
validationRules: Record<string, ValidationRule>;
}

export interface ValidationRule {
required?: boolean;
pattern?: RegExp;
min?: number;
max?: number;
message?: string;
}

// useForm.ts
import { ref, reactive } from 'vue';
import type { FormField, FormOptions, ValidationRule } from './types';

export function useForm<T extends Record<string, any>>(options: FormOptions) {
const form = reactive({ ...options.initialValues }) as T;
const errors = reactive<Record<string, string>>({});

// ...其他实现
}

3. 性能优化

// 使用computed避免不必要的重新计算
import { computed } from 'vue';

export function useFilteredList(data, filters) {
const filteredData = computed(() => {
return data.value.filter(item => {
return Object.keys(filters).every(key => {
const filter = filters[key];
if (!filter) return true;

// 根据过滤条件判断
switch (key) {
case 'search':
return item.name.toLowerCase().includes(filter.toLowerCase());
case 'category':
return item.category === filter;
case 'price':
return item.price <= filter.max && item.price >= filter.min;
default:
return true;
}
});
});
});

return { filteredData };
}

4. 错误处理

// useErrorHandler.js
import { ref } from 'vue';

export function useErrorHandler() {
const error = ref(null);

const handleError = (err) => {
console.error('Error:', err);
error.value = err;

// 可以在这里添加错误上报逻辑
if (process.env.NODE_ENV === 'production') {
// 上报错误到监控系统
reportError(err);
}
};

const clearError = () => {
error.value = null;
};

return { error, handleError, clearError };
}

四、实战案例

1. 可复用的图表组件

// useChart.js
import { ref, watch, onMounted, onUnmounted } from 'vue';
import * as echarts from 'echarts';

export function useChart(containerId, options) {
const chartInstance = ref(null);
const container = ref(null);

const initChart = () => {
if (container.value) {
chartInstance.value = echarts.init(container.value);

// 监听数据变化
watch(options, (newOptions) => {
if (chartInstance.value) {
chartInstance.value.setOption(newOptions);
}
}, { deep: true });

// 监听窗口大小变化
const resizeObserver = new ResizeObserver(() => {
if (chartInstance.value) {
chartInstance.value.resize();
}
});

resizeObserver.observe(container.value);

onUnmounted(() => {
if (chartInstance.value) {
chartInstance.value.dispose();
}
resizeObserver.disconnect();
});
}
};

onMounted(initChart);

return {
container,
chartInstance,
updateChart: (newOptions) => {
if (chartInstance.value) {
chartInstance.value.setOption(newOptions, true);
}
}
};
}

使用方式:

<template>
<div ref="chartContainer" style="width: 100%; height: 400px;"></div>
</template>

<script>
import { useChart } from './composables/useChart';

export default {
setup() {
const { container, updateChart } = useChart('chartContainer', {
title: {
text: '销售趋势'
},
tooltip: {},
xAxis: {
data: ['1月', '2月', '3月', '4月', '5月', '6月']
},
yAxis: {},
series: [{
name: '销量',
type: 'line',
data: [150, 230, 224, 218, 135, 147]
}]
});

return { container };
}
}
</script>

2. 拖拽功能组合式函数

// useDraggable.js
import { ref, computed, onMounted, onUnmounted } from 'vue';

export function useDraggable(elementRef, options = {}) {
const isDragging = ref(false);
const position = ref({ x: 0, y: 0 });
const offset = ref({ x: 0, y: 0 });

const style = computed(() => ({
position: 'fixed',
left: `${position.value.x}px`,
top: `${position.value.y}px`,
cursor: isDragging.value ? 'grabbing' : 'grab',
zIndex: isDragging.value ? 1000 : options.zIndex || 1,
transform: isDragging.value ? 'scale(1.05)' : 'scale(1)',
transition: isDragging.value ? 'none' : 'transform 0.2s ease'
}));

const startDrag = (event) => {
isDragging.value = true;

const rect = elementRef.value.getBoundingClientRect();
offset.value = {
x: event.clientX - rect.left,
y: event.clientY - rect.top
};

document.addEventListener('mousemove', drag);
document.addEventListener('mouseup', stopDrag);

// 防止文本选中
document.body.style.userSelect = 'none';
};

const drag = (event) => {
if (!isDragging.value) return;

position.value = {
x: event.clientX - offset.value.x,
y: event.clientY - offset.value.y
};
};

const stopDrag = () => {
isDragging.value = false;
document.removeEventListener('mousemove', drag);
document.removeEventListener('mouseup', stopDrag);
document.body.style.userSelect = '';
};

// 限制拖拽范围
const constrainPosition = (x, y) => {
const maxX = window.innerWidth - elementRef.value.offsetWidth;
const maxY = window.innerHeight - elementRef.value.offsetHeight;

return {
x: Math.max(0, Math.min(x, maxX)),
y: Math.max(0, Math.min(y, maxY))
};
};

onMounted(() => {
elementRef.value.addEventListener('mousedown', startDrag);
});

onUnmounted(() => {
elementRef.value.removeEventListener('mousedown', startDrag);
stopDrag();
});

return {
isDragging,
position,
style,
constrainPosition
};
}

使用方式:

<template>
<div
:style="style"
class="draggable-element"
>
<h3>拖拽我</h3>
<p>位置: {{ position.x }}, {{ position.y }}</p>
</div>
</template>

<script>
import { useDraggable } from './composables/useDraggable';

export default {
setup() {
const elementRef = ref(null);
const { isDragging, position, style } = useDraggable(elementRef);

return { elementRef, isDragging, position, style };
}
}
</script>

<style>
.draggable-element {
background: white;
padding: 20px;
border-radius: 8px;
box-shadow: 0 2px 8px rgba(0,0,0,0.1);
user-select: none;
}
</style>

3. 权限管理组合式函数

// usePermissions.js
import { ref, computed } from 'vue';

export function usePermissions() {
const userRoles = ref(['user']); // 从用户信息中获取

const hasRole = computed(() => (role) => {
return userRoles.value.includes(role);
});

const hasPermission = computed(() => (permission) => {
// 这里可以根据权限系统进行判断
// 简单示例:管理员拥有所有权限
if (hasRole.value('admin')) return true;

// 普通用户权限
const userPermissions = {
'user:read': ['user', 'editor'],
'user:write': ['editor'],
'user:delete': ['admin']
};

return userPermissions[permission]?.includes(userRoles.value[0]) || false;
});

const checkAccess = (requiredPermission) => {
return hasPermission.value(requiredPermission);
};

return {
hasRole,
hasPermission,
checkAccess
};
}

使用方式:

<template>
<div>
<button v-if="checkAccess('user:write')" @click="editPost">
编辑文章
</button>
<button v-if="checkAccess('user:delete')" @click="deletePost">
删除文章
</button>
</div>
</template>

<script>
import { usePermissions } from './composables/usePermissions';

export default {
setup() {
const { checkAccess } = usePermissions();

const editPost = () => {
console.log('编辑文章');
};

const deletePost = () => {
console.log('删除文章');
};

return { checkAccess, editPost, deletePost };
}
}
</script>

五、常见问题解决

1. 响应式丢失问题

问题:在组合式函数中,返回的响应式数据可能会丢失响应性。

// 错误:直接返回对象
export function useCounter() {
const count = ref(0);

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

const getCount = () => {
return count.value; // 失去响应性
};

return {
count,
increment,
getCount
};
}

// 正确:保持响应性
export function useCounterCorrect() {
const count = ref(0);

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

return {
count,
increment,
doubleCount: computed(() => count.value * 2)
};
}

2. 依赖注入问题

问题:组合式函数之间的依赖关系不清晰。

// 好的做法:明确依赖
export function useUserSettings() {
const { theme } = useTheme();
const { language } = useLanguage();

const settings = computed(() => ({
theme: theme.value,
language: language.value
}));

return { settings };
}

// 避免:隐式依赖
export function useUserSettingsBad() {
const theme = ref('light');
const language = ref('zh-CN');

// 这里隐式依赖了其他状态,难以维护
}

3. 内存泄漏问题

问题:组合式函数中未清理的事件监听器。

// 正确:清理资源
export function useWebSocket(url) {
const socket = ref(null);
const data = ref(null);

const connect = () => {
socket.value = new WebSocket(url);

socket.value.onmessage = (event) => {
data.value = JSON.parse(event.data);
};

socket.value.onclose = () => {
// 自动重连
setTimeout(connect, 1000);
};
};

onUnmounted(() => {
if (socket.value) {
socket.value.close();
}
});

return { data, connect };
}

六、总结

Vue 3的Composition API为我们提供了更加灵活和强大的代码组织方式。通过组合式函数,我们能够更好地复用逻辑、提高代码质量,并且使组件更加清晰和易于维护。

在实际项目中,我发现组合式函数特别适合处理复杂的业务逻辑,比如表单处理、数据获取、状态管理等。但是,也要注意不要过度使用,保持代码的简洁和可读性。

希望这篇文章能够帮助你更好地理解和使用Vue 3的Composition API。如果你有任何问题或建议,欢迎在评论区交流!


本文由前端开发者原创,如需转载请注明出处。