在当今前端开发领域,组件化设计已成为提升开发效率的关键。Vue3凭借其出色的组合式API和性能优化,为开发者提供了更强大的组件封装能力。本文将深入探讨如何基于Vue3构建一个功能丰富的CardGroup组件,不仅实现基础的网格布局,还将扩展瀑布流布局和多种动态Hover特效,满足现代Web应用对交互体验的高要求。
一个优秀的CardGroup组件应当遵循以下设计原则:
我们将使用Vue3的组合式API进行开发,主要依赖以下技术栈:
bash复制# 项目初始化
npm init vue@latest vue-card-group
cd vue-card-group
npm install
核心依赖版本建议:
首先构建基础的Card组件,作为CardGroup的子元素:
vue复制<template>
<div
class="card"
:class="hoverEffect"
@click="handleClick"
:style="{ backgroundColor }"
>
<div class="card-header" v-if="$slots.header || title">
<slot name="header">
{{ title }}
</slot>
</div>
<div class="card-body">
<slot></slot>
</div>
</div>
</template>
<script setup lang="ts">
defineProps({
title: String,
hoverEffect: {
type: String,
default: 'scale'
},
backgroundColor: String
})
const emit = defineEmits(['click'])
const handleClick = (event: Event) => {
emit('click', event)
}
</script>
接下来实现基础的网格布局功能:
vue复制<template>
<div
class="card-group"
:style="{
'--columns': columns,
'--gap': `${gap}px`
}"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
defineProps({
columns: {
type: Number,
default: 3
},
gap: {
type: Number,
default: 16
}
})
</script>
<style scoped>
.card-group {
display: grid;
grid-template-columns: repeat(var(--columns), 1fr);
gap: var(--gap);
}
</style>
瀑布流布局的核心在于:
| 方案 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| CSS Grid | 实现简单,性能较好 | 无法实现真正的瀑布流 | 内容高度差异不大的场景 |
| JavaScript计算 | 灵活性高,效果精确 | 实现复杂,性能开销大 | 内容高度差异大的专业场景 |
| CSS Columns | 实现简单,兼容性好 | 列顺序是垂直排列 | 对顺序要求不高的场景 |
我们选择使用ResizeObserver API来实现高性能的瀑布流布局:
typescript复制// useWaterfall.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useWaterfall(containerRef: Ref<HTMLElement | null>, options = { gap: 16 }) {
const positions = ref<Array<{top: number, left: number}>>([])
const observer = new ResizeObserver((entries) => {
calculateLayout()
})
onMounted(() => {
if (containerRef.value) {
Array.from(containerRef.value.children).forEach(child => {
observer.observe(child)
})
}
calculateLayout()
})
onUnmounted(() => {
observer.disconnect()
})
const calculateLayout = () => {
if (!containerRef.value) return
const containerWidth = containerRef.value.clientWidth
const children = Array.from(containerRef.value.children) as HTMLElement[]
// 实现瀑布流布局算法
// ...
}
return { positions }
}
我们将实现以下三种特效:
vue复制<template>
<div
class="card"
:class="`hover-${hoverEffect}`"
@mousemove="handleMouseMove"
:style="{
'--x': mouseX,
'--y': mouseY
}"
>
<!-- 卡片内容 -->
</div>
</template>
<script setup lang="ts">
const mouseX = ref(0)
const mouseY = ref(0)
const handleMouseMove = (e: MouseEvent) => {
const rect = e.currentTarget.getBoundingClientRect()
mouseX.value = ((e.clientX - rect.left) / rect.width) * 100
mouseY.value = ((e.clientY - rect.top) / rect.height) * 100
}
</script>
<style scoped>
.hover-3d {
transition: transform 0.5s ease;
transform-style: preserve-3d;
}
.hover-3d:hover {
transform: rotateY(15deg) rotateX(10deg);
}
.hover-light {
position: relative;
overflow: hidden;
}
.hover-light::before {
content: '';
position: absolute;
top: calc(var(--y) * 1%);
left: calc(var(--x) * 1%);
width: 200%;
height: 200%;
background: radial-gradient(circle, rgba(255,255,255,0.3) 0%, transparent 70%);
transform: translate(-50%, -50%);
opacity: 0;
transition: opacity 0.3s;
}
.hover-light:hover::before {
opacity: 1;
}
.hover-zoom .card-body {
transition: transform 0.3s ease;
}
.hover-zoom:hover .card-body {
transform: scale(1.05);
}
</style>
css复制.card {
will-change: transform, opacity;
}
为不同屏幕尺寸设置不同的列数:
typescript复制// useResponsive.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useResponsive() {
const columns = ref(3)
const updateColumns = () => {
const width = window.innerWidth
if (width < 768) {
columns.value = 1
} else if (width < 1024) {
columns.value = 2
} else {
columns.value = 3
}
}
onMounted(() => {
updateColumns()
window.addEventListener('resize', updateColumns)
})
onUnmounted(() => {
window.removeEventListener('resize', updateColumns)
})
return { columns }
}
使用TypeScript增强组件类型安全:
typescript复制type HoverEffect = '3d' | 'light' | 'zoom' | 'none'
interface CardProps {
title?: string
hoverEffect?: HoverEffect
backgroundColor?: string
}
const props = withDefaults(defineProps<CardProps>(), {
hoverEffect: 'zoom'
})
vue复制<template>
<div
ref="container"
class="card-group"
:class="{ 'waterfall': layout === 'waterfall' }"
>
<slot></slot>
</div>
</template>
<script setup lang="ts">
import { ref, watch, onMounted } from 'vue'
import { useWaterfall } from './useWaterfall'
import { useResponsive } from './useResponsive'
const props = defineProps({
layout: {
type: String as () => 'grid' | 'waterfall',
default: 'grid'
},
columns: {
type: Number,
default: 0 // 0表示自动响应式
},
gap: {
type: Number,
default: 16
}
})
const container = ref<HTMLElement | null>(null)
const { columns: responsiveColumns } = useResponsive()
const actualColumns = computed(() => {
return props.columns > 0 ? props.columns : responsiveColumns.value
})
const { positions } = useWaterfall(container, {
gap: props.gap,
enabled: props.layout === 'waterfall'
})
watch(() => props.layout, () => {
// 布局变化时重新计算
})
</script>
<style scoped>
.card-group {
display: grid;
grid-template-columns: repeat(v-bind('actualColumns'), 1fr);
gap: v-bind('props.gap + "px"');
}
.card-group.waterfall {
display: block;
position: relative;
}
.card-group.waterfall .card {
position: absolute;
width: calc((100% - (v-bind('actualColumns - 1') * v-bind('props.gap + "px"'))) / v-bind('actualColumns'));
}
</style>
vue复制<template>
<CardGroup layout="waterfall" :columns="3">
<Card
v-for="(item, index) in items"
:key="index"
:title="item.title"
:hover-effect="item.effect"
>
<img :src="item.image" alt="">
<p>{{ item.description }}</p>
</Card>
</CardGroup>
</template>
<script setup lang="ts">
import { ref } from 'vue'
import Card from './Card.vue'
import CardGroup from './CardGroup.vue'
const items = ref([
{
title: '产品1',
image: '/images/product1.jpg',
description: '这是产品1的描述内容',
effect: '3d'
},
// 更多数据...
])
</script>
使用Vitest编写组件测试:
typescript复制import { mount } from '@vue/test-utils'
import CardGroup from '../CardGroup.vue'
describe('CardGroup', () => {
it('renders correct number of columns', async () => {
const wrapper = mount(CardGroup, {
props: { columns: 4 },
slots: {
default: '<div class="test-card"></div>'.repeat(8)
}
})
expect(wrapper.find('.card-group').attributes('style')).toContain('grid-template-columns: repeat(4, 1fr)')
})
})
| 指标 | 目标值 | 测试方法 |
|---|---|---|
| 首次渲染时间 | <100ms | Chrome DevTools Performance |
| Hover响应时间 | <50ms | Event Timing API |
| 布局计算时间 | <30ms | console.time |
| 内存占用 | <10MB | Memory Profiler |
卡片闪烁问题:
布局错位问题:
性能瓶颈:
如果计划将组件集成到UI库中,建议:
在实际项目中,这种CardGroup组件特别适合产品展示、图片画廊、仪表盘等场景。通过合理的参数配置,可以轻松适应不同的设计需求,同时保持优秀的性能表现。