前言
随着前端技术的快速发展,Vue 3 的发布为开发者带来了全新的开发体验。其中最引人注目的特性之一就是 Composition API,它让开发者能够更灵活地组织和复用逻辑代码。在现代前端开发中,构建可复用、可维护的组件库已成为提升开发效率的关键。
本文将深入探讨 Vue 3 Composition API 的高级用法,通过实际案例展示如何封装通用逻辑、创建可复用的组合式函数,并最终构建一个响应式的组件库。我们将从基础概念出发,逐步深入到复杂场景的应用实践,帮助开发者掌握这一强大的工具。
Vue 3 Composition API 核心概念
什么是 Composition API
Composition API 是 Vue 3 中引入的一种新的组件逻辑组织方式。与传统的 Options API 不同,Composition API 允许我们以函数的形式组织和复用组件逻辑,使得代码更加灵活和可组合。
// 传统 Options API
export default {
data() {
return {
count: 0,
message: ''
}
},
methods: {
increment() {
this.count++
}
},
computed: {
doubledCount() {
return this.count * 2
}
}
}
// Composition API
import { ref, computed } from 'vue'
export default {
setup() {
const count = ref(0)
const message = ref('')
const doubledCount = computed(() => count.value * 2)
const increment = () => {
count.value++
}
return {
count,
message,
doubledCount,
increment
}
}
}
Composition API 的优势
- 逻辑复用:通过组合式函数,可以轻松地在多个组件间共享逻辑
- 更好的类型支持:TypeScript 集成更加自然
- 更灵活的代码组织:按照功能而非选项类型来组织代码
- 更小的包体积:按需引入,减少不必要的代码
创建可复用的组合式函数
基础组合式函数示例
让我们从一个简单的例子开始,创建一个用于处理表单验证的组合式函数:
// composables/useFormValidation.js
import { ref, reactive, computed } from 'vue'
export function useFormValidation(initialData = {}) {
const formData = reactive({ ...initialData })
const errors = ref({})
const isValid = computed(() => Object.keys(errors.value).length === 0)
const validateField = (fieldName, value, rules) => {
if (!rules || rules.length === 0) return ''
for (const rule of rules) {
if (rule.required && !value) {
return rule.message || `${fieldName} 是必填项`
}
if (rule.minLength && value.length < rule.minLength) {
return rule.message || `${fieldName} 长度不能少于 ${rule.minLength} 位`
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || `${fieldName} 格式不正确`
}
}
return ''
}
const validate = (fieldRules) => {
const newErrors = {}
for (const [fieldName, rules] of Object.entries(fieldRules)) {
const value = formData[fieldName]
const error = validateField(fieldName, value, rules)
if (error) {
newErrors[fieldName] = error
}
}
errors.value = newErrors
return isValid.value
}
const setFieldValue = (fieldName, value) => {
formData[fieldName] = value
// 实时验证
if (errors.value[fieldName]) {
const fieldRules = {} // 需要根据实际场景获取规则
validateField(fieldName, value, fieldRules[fieldName])
}
}
return {
formData,
errors,
isValid,
validate,
setFieldValue
}
}
高级组合式函数:数据获取和缓存
// composables/useApi.js
import { ref, watch } from 'vue'
export function useApi(apiFunction, options = {}) {
const data = ref(null)
const loading = ref(false)
const error = ref(null)
const cache = new Map()
const {
cacheKey = null,
autoLoad = true,
refreshOnMount = false
} = options
const fetchData = async (params = {}) => {
try {
loading.value = true
error.value = null
// 检查缓存
if (cacheKey && cache.has(cacheKey)) {
data.value = cache.get(cacheKey)
return data.value
}
const result = await apiFunction(params)
data.value = result
// 存储到缓存
if (cacheKey) {
cache.set(cacheKey, result)
}
return result
} catch (err) {
error.value = err
throw err
} finally {
loading.value = false
}
}
const refresh = async () => {
if (cacheKey && cache.has(cacheKey)) {
cache.delete(cacheKey)
}
return fetchData()
}
// 自动加载
if (autoLoad) {
fetchData()
}
// 监听参数变化并重新获取数据
watch(() => data.value, () => {
// 可以在这里添加数据变更后的逻辑
})
return {
data,
loading,
error,
refresh,
fetchData
}
}
状态管理组合式函数
// composables/useGlobalState.js
import { ref, reactive, watch } from 'vue'
export function useGlobalState(initialState = {}) {
const state = reactive({ ...initialState })
// 持久化存储
const persistKey = '__global_state__'
const loadFromStorage = () => {
try {
const stored = localStorage.getItem(persistKey)
if (stored) {
Object.assign(state, JSON.parse(stored))
}
} catch (error) {
console.error('Failed to load state from storage:', error)
}
}
const saveToStorage = () => {
try {
localStorage.setItem(persistKey, JSON.stringify(state))
} catch (error) {
console.error('Failed to save state to storage:', error)
}
}
// 监听状态变化并保存到存储
watch(state, saveToStorage, { deep: true })
// 初始化时从存储加载
loadFromStorage()
const setState = (key, value) => {
state[key] = value
}
const resetState = () => {
Object.assign(state, initialState)
}
return {
state,
setState,
resetState,
loadFromStorage,
saveToStorage
}
}
构建响应式组件库
组件库架构设计
一个良好的组件库应该具备以下特性:
- 模块化:每个组件独立,可按需引入
- 可配置性:支持丰富的配置选项
- 可扩展性:易于扩展和定制
- 类型安全:提供完整的 TypeScript 支持
// components/DataTable.vue
<template>
<div class="data-table">
<div class="table-header">
<div class="search-box" v-if="showSearch">
<input
v-model="searchQuery"
placeholder="搜索..."
class="search-input"
/>
</div>
<div class="actions" v-if="showActions">
<button @click="refreshData" class="btn-refresh">
刷新
</button>
<slot name="actions"></slot>
</div>
</div>
<div class="table-container">
<table class="table">
<thead>
<tr>
<th
v-for="column in columns"
:key="column.key"
@click="sortColumn(column.key)"
>
{{ column.title }}
<span v-if="sortBy === column.key" class="sort-indicator">
{{ sortOrder === 'asc' ? '↑' : '↓' }}
</span>
</th>
</tr>
</thead>
<tbody>
<tr v-for="row in filteredData" :key="row.id">
<td v-for="column in columns" :key="column.key">
<component
:is="column.component || 'span'"
:data="row"
:value="row[column.key]"
v-bind="column.props || {}"
>
{{ formatValue(row[column.key], column) }}
</component>
</td>
</tr>
</tbody>
</table>
</div>
<div class="pagination" v-if="showPagination">
<button
@click="prevPage"
:disabled="currentPage === 1"
class="btn-page"
>
上一页
</button>
<span class="page-info">
{{ currentPage }} / {{ totalPages }}
</span>
<button
@click="nextPage"
:disabled="currentPage === totalPages"
class="btn-page"
>
下一页
</button>
</div>
</div>
</template>
<script setup>
import { ref, computed, watch } from 'vue'
import { useApi } from '../composables/useApi'
const props = defineProps({
columns: {
type: Array,
required: true
},
apiUrl: {
type: String,
required: true
},
pageSize: {
type: Number,
default: 10
},
showSearch: {
type: Boolean,
default: true
},
showActions: {
type: Boolean,
default: true
},
showPagination: {
type: Boolean,
default: true
}
})
const emit = defineEmits(['row-click', 'data-loaded'])
// 组合式函数
const { data, loading, error, fetchData } = useApi(
(params) => fetch(props.apiUrl, { params }),
{ autoLoad: false }
)
// 响应式状态
const searchQuery = ref('')
const currentPage = ref(1)
const sortBy = ref('')
const sortOrder = ref('asc')
// 计算属性
const filteredData = computed(() => {
if (!data.value) return []
let result = [...data.value.data]
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(query)
)
)
}
// 排序
if (sortBy.value) {
result.sort((a, b) => {
const aValue = a[sortBy.value]
const bValue = b[sortBy.value]
if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1
if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1
return 0
})
}
return result
})
const totalPages = computed(() => {
if (!data.value) return 1
return Math.ceil(data.value.total / props.pageSize)
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * props.pageSize
const end = start + props.pageSize
return filteredData.value.slice(start, end)
})
// 方法
const refreshData = async () => {
await fetchData()
emit('data-loaded', data.value)
}
const sortColumn = (columnKey) => {
if (sortBy.value === columnKey) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = columnKey
sortOrder.value = 'asc'
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
const formatValue = (value, column) => {
if (column.formatter) {
return column.formatter(value)
}
return value
}
// 监听数据变化
watch(data, () => {
currentPage.value = 1
})
// 初始化加载数据
refreshData()
</script>
<style scoped>
.data-table {
width: 100%;
border: 1px solid #ddd;
border-radius: 4px;
}
.table-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 16px;
border-bottom: 1px solid #eee;
}
.search-box {
flex: 1;
max-width: 300px;
}
.search-input {
width: 100%;
padding: 8px 12px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 14px;
}
.actions {
display: flex;
gap: 8px;
}
.btn-refresh, .btn-page {
padding: 8px 16px;
border: 1px solid #ddd;
background: white;
border-radius: 4px;
cursor: pointer;
}
.btn-refresh:hover, .btn-page:hover {
background: #f5f5f5;
}
.table-container {
overflow-x: auto;
}
.table {
width: 100%;
border-collapse: collapse;
}
.table th,
.table td {
padding: 12px;
text-align: left;
border-bottom: 1px solid #eee;
}
.table th {
background-color: #f8f9fa;
font-weight: 600;
cursor: pointer;
user-select: none;
}
.table th:hover {
background-color: #e9ecef;
}
.sort-indicator {
margin-left: 4px;
font-size: 12px;
}
.pagination {
display: flex;
justify-content: center;
align-items: center;
padding: 16px;
gap: 8px;
}
.page-info {
font-size: 14px;
color: #666;
}
</style>
自定义 Hook 的使用
// composables/useDataTable.js
import { ref, computed, watch } from 'vue'
import { useApi } from './useApi'
export function useDataTable(props) {
const {
apiUrl,
pageSize = 10,
showSearch = true,
showActions = true,
showPagination = true
} = props
// API 状态
const { data, loading, error, fetchData, refresh } = useApi(
(params) => fetch(apiUrl, { params }),
{ autoLoad: false }
)
// 表格状态
const searchQuery = ref('')
const currentPage = ref(1)
const sortBy = ref('')
const sortOrder = ref('asc')
// 计算属性
const filteredData = computed(() => {
if (!data.value) return []
let result = [...data.value.data]
// 搜索过滤
if (searchQuery.value) {
const query = searchQuery.value.toLowerCase()
result = result.filter(item =>
Object.values(item).some(value =>
String(value).toLowerCase().includes(query)
)
)
}
// 排序
if (sortBy.value) {
result.sort((a, b) => {
const aValue = a[sortBy.value]
const bValue = b[sortBy.value]
if (aValue < bValue) return sortOrder.value === 'asc' ? -1 : 1
if (aValue > bValue) return sortOrder.value === 'asc' ? 1 : -1
return 0
})
}
return result
})
const totalPages = computed(() => {
if (!data.value) return 1
return Math.ceil(data.value.total / pageSize)
})
const paginatedData = computed(() => {
const start = (currentPage.value - 1) * pageSize
const end = start + pageSize
return filteredData.value.slice(start, end)
})
// 方法
const refreshData = async () => {
await refresh()
}
const sortColumn = (columnKey) => {
if (sortBy.value === columnKey) {
sortOrder.value = sortOrder.value === 'asc' ? 'desc' : 'asc'
} else {
sortBy.value = columnKey
sortOrder.value = 'asc'
}
}
const prevPage = () => {
if (currentPage.value > 1) {
currentPage.value--
}
}
const nextPage = () => {
if (currentPage.value < totalPages.value) {
currentPage.value++
}
}
// 监听数据变化
watch(data, () => {
currentPage.value = 1
})
return {
data,
loading,
error,
searchQuery,
currentPage,
sortBy,
sortOrder,
filteredData,
totalPages,
paginatedData,
refreshData,
sortColumn,
prevPage,
nextPage
}
}
最佳实践和性能优化
组件性能优化
// components/OptimizedList.vue
<template>
<div class="optimized-list">
<div
v-for="item in visibleItems"
:key="item.id"
class="list-item"
@click="$emit('item-click', item)"
>
{{ item.name }}
</div>
<!-- 虚拟滚动 -->
<div
v-if="virtualScroll"
class="scroll-placeholder"
:style="{ height: totalHeight + 'px' }"
></div>
</div>
</template>
<script setup>
import { ref, computed, onMounted, onUnmounted } from 'vue'
const props = defineProps({
items: {
type: Array,
required: true
},
itemHeight: {
type: Number,
default: 50
},
visibleCount: {
type: Number,
default: 10
},
virtualScroll: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['item-click'])
// 滚动相关状态
const scrollTop = ref(0)
const containerHeight = ref(0)
// 计算可见项
const visibleItems = computed(() => {
if (!props.virtualScroll) {
return props.items.slice(0, props.visibleCount)
}
const startIndex = Math.floor(scrollTop.value / props.itemHeight)
const endIndex = Math.min(
startIndex + Math.ceil(containerHeight.value / props.itemHeight),
props.items.length
)
return props.items.slice(startIndex, endIndex)
})
const totalHeight = computed(() => {
return props.items.length * props.itemHeight
})
// 滚动处理
const handleScroll = (event) => {
scrollTop.value = event.target.scrollTop
}
// 初始化容器高度
onMounted(() => {
const container = document.querySelector('.optimized-list')
if (container) {
containerHeight.value = container.clientHeight
}
})
onUnmounted(() => {
// 清理资源
})
</script>
<style scoped>
.optimized-list {
height: 400px;
overflow-y: auto;
border: 1px solid #ddd;
}
.list-item {
padding: 12px;
border-bottom: 1px solid #eee;
cursor: pointer;
transition: background-color 0.2s;
}
.list-item:hover {
background-color: #f5f5f5;
}
.scroll-placeholder {
width: 100%;
}
</style>
响应式数据管理
// composables/useReactiveData.js
import { ref, reactive, computed, watch } from 'vue'
export function useReactiveData(initialData = {}) {
const data = reactive({ ...initialData })
const loading = ref(false)
const error = ref(null)
// 数据操作方法
const setData = (key, value) => {
data[key] = value
}
const updateData = (updates) => {
Object.assign(data, updates)
}
const deleteData = (key) => {
delete data[key]
}
// 计算属性
const isEmpty = computed(() => {
return Object.keys(data).length === 0
})
const hasKey = (key) => {
return key in data
}
// 监听数据变化
watch(data, () => {
// 数据变更后的副作用处理
}, { deep: true })
// 返回所有可访问的属性
return {
data,
loading,
error,
setData,
updateData,
deleteData,
isEmpty,
hasKey
}
}
实际应用场景
表单组件库构建
// components/FormBuilder.vue
<template>
<form @submit.prevent="handleSubmit" class="form-builder">
<div
v-for="field in fields"
:key="field.name"
class="form-field"
>
<label v-if="field.label" class="field-label">
{{ field.label }}
<span v-if="field.required" class="required">*</span>
</label>
<component
:is="getFieldComponent(field.type)"
v-model="formData[field.name]"
:field="field"
:errors="errors[field.name]"
@input="handleFieldInput"
/>
<div v-if="errors[field.name]" class="error-message">
{{ errors[field.name] }}
</div>
</div>
<div class="form-actions">
<button type="submit" :disabled="loading || !isValid" class="btn-submit">
{{ loading ? '提交中...' : '提交' }}
</button>
<button
type="button"
@click="resetForm"
class="btn-reset"
>
重置
</button>
</div>
</form>
</template>
<script setup>
import { ref, reactive, computed } from 'vue'
import { useFormValidation } from '../composables/useFormValidation'
const props = defineProps({
fields: {
type: Array,
required: true
},
initialData: {
type: Object,
default: () => ({})
}
})
const emit = defineEmits(['submit', 'reset'])
// 表单验证
const { formData, errors, isValid, validate } = useFormValidation(props.initialData)
// 状态管理
const loading = ref(false)
const submitted = ref(false)
// 字段组件映射
const getFieldComponent = (type) => {
const components = {
text: 'InputField',
select: 'SelectField',
textarea: 'TextAreaField',
checkbox: 'CheckboxField',
radio: 'RadioField'
}
return components[type] || 'InputField'
}
// 处理字段输入
const handleFieldInput = (field, value) => {
formData[field.name] = value
// 实时验证
if (errors.value[field.name]) {
validateField(field.name, value)
}
}
// 验证单个字段
const validateField = (fieldName, value) => {
const field = props.fields.find(f => f.name === fieldName)
if (!field || !field.rules) return
const error = validateFieldRule(value, field.rules)
if (error) {
errors.value[fieldName] = error
} else {
delete errors.value[fieldName]
}
}
// 提交表单
const handleSubmit = async () => {
if (!isValid.value) {
validate()
return
}
loading.value = true
try {
await emit('submit', { ...formData })
submitted.value = true
} catch (err) {
console.error('Form submission error:', err)
} finally {
loading.value = false
}
}
// 重置表单
const resetForm = () => {
Object.keys(formData).forEach(key => {
formData[key] = props.initialData[key] || ''
})
errors.value = {}
submitted.value = false
emit('reset')
}
// 字段验证规则
const validateFieldRule = (value, rules) => {
for (const rule of rules) {
if (rule.required && !value) {
return rule.message || '此字段为必填项'
}
if (rule.minLength && value.length < rule.minLength) {
return rule.message || `最少需要 ${rule.minLength} 个字符`
}
if (rule.pattern && !rule.pattern.test(value)) {
return rule.message || '输入格式不正确'
}
}
return ''
}
// 初始化表单数据
Object.keys(props.initialData).forEach(key => {
formData[key] = props.initialData[key]
})
</script>
<style scoped>
.form-builder {
max-width: 600px;
margin: 0 auto;
padding: 20px;
}
.form-field {
margin-bottom: 20px;
}
.field-label {
display: block;
margin-bottom: 8px;
font-weight: 500;
}
.required {
color: #e74c3c;
}
.error-message {
color: #e74c3c;
font-size: 12px;
margin-top: 4px;
}
.form-actions {
display: flex;
gap: 12px;
margin-top: 30px;
}
.btn-submit, .btn-reset {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 14px;
}
.btn-submit {
background-color: #3498db;
color: white;
}
.btn-submit:disabled {
background-color: #bdc3c7;
cursor: not-allowed;
}
.btn-reset {
background-color: #95a5a6;
color: white;
}
</style>
总结与展望
通过本文的深入探讨,我们看到了 Vue 3 Composition API 在构建可复用组件库方面的强大能力。从基础的组合式函数创建,到复杂的响应式数据管理,再到实际的组件实现,Composition API 提供了前所未有的灵活性和可组合性。
关键收获
- 逻辑复用:通过组合式函数,可以将通用逻辑封装成独立的可复用单元
- 状态管理:更直观的状态管理和响应式数据处理方式
- 性能优化:结合虚拟滚动、懒加载等技术提升组件性能
- 类型安全:更好的 TypeScript 支持让代码更加健壮
未来发展趋势
随着 Vue 生态系统的不断发展,Composition API 将会:
- 进一步完善:更多的内置组合式函数和工具函数 2

评论 (0)