在上一篇文章 《从零打造 AI 全栈应用(三):一个 BackToTop 组件背后的工程化与性能思维》中,我们拆解了一个看似简单却极易被忽视的组件,并通过它讨论了事件监听、性能优化与工程边界的问题。
但真实项目中,工程能力往往体现在细节。
这篇文章,我们从一个再常见不过的组件 —— 幻灯片 / Carousel 入手,聊聊它背后隐藏的组件设计、状态管理、性能优化和工程化取舍。
一、为什么一个幻灯片组件值得单独写一篇?
很多同学会觉得:
幻灯片不就是个 UI 组件吗?能滑就行。
但在真实项目(尤其是首页、活动页、AI 产品的内容入口)中,它往往意味着:
- 页面首屏核心组件
- 高频渲染、长时间驻留
- 涉及自动播放、交互、状态同步
- 很容易成为性能与可维护性的隐患
所以我们这次的目标不是「写一个能跑的轮播图」,而是:
写一个工程上站得住脚的 Carousel 组件。
二、技术选型:为什么是 shadcn/ui + Embla?
1️⃣ shadcn/ui 的 Carousel 设计哲学
shadcn/ui 提供的并不是一个“黑盒组件”,而是一组组合式组件:
CarouselCarouselContentCarouselItem
特点很明显:
- 结构清晰,层次分明
- 不强绑定样式
- 底层基于 Embla Carousel,性能成熟
本质上,它更像是一个“轮播能力的外壳”,而不是一个定死的 UI。
这点非常重要 —— 可定制性 = 长期可维护性。
2️⃣ 自动播放为什么用插件,而不是自己写 setInterval?
自动播放我们选择的是:
import AutoPlay from 'embla-carousel-autoplay'
原因很简单:
- 和 Embla API 深度适配
- 内部处理了生命周期、交互中断
- 不需要自己处理定时器清理
工程中一个重要原则:
能用成熟插件解决的,不要重复造轮子。
三、组件设计:从 Props 到职责边界
1️⃣ 数据结构设计
export interface SlideData {
id: number | string;
image: string;
title?: string;
}
几个刻意的设计点:
id不强制 number,兼容服务端数据title可选,UI 自动适配- 组件只关心展示所需的最小数据
不要把组件变成业务垃圾桶。
2️⃣ 组件 Props:只暴露真正需要的能力
interface SlideShowProps {
slides: SlideData[];
autoPlay?: boolean;
autoPlayDelay?: number;
}
这里没有:
- 当前 index 的受控状态
- 复杂回调
原因是:
这是一个偏展示型组件,而不是业务中枢。
四、状态管理:selectedIndex 为什么是私有状态?
const [selectedIndex, setSelectedIndex] = useState<number>(0);
const [api, setApi] = useState<CarouselApi | null>(null);
关键点:
selectedIndex不从外部传入- 通过
CarouselApi与底层同步
useEffect(() => {
if (!api) return;
setSelectedIndex(api.selectedScrollSnap());
const onSelect = () => {
setSelectedIndex(api.selectedScrollSnap());
};
api.on('select', onSelect);
return () => api.off('select', onSelect);
}, [api]);
这段代码的工程意义:
- UI 状态 来源单一(底层 carousel)
- React 状态只是一个映射
- 避免“双向状态不同步”
面试常考:如何避免状态源混乱?
这个例子非常典型。
五、自动播放:为什么一定要用 useRef?
const plugin = useRef(
autoPlay
? AutoPlay({ delay: autoPlayDelay, stopOnInteraction: true })
: null
);
如果不用 useRef 会发生什么?
- 每次 render 都创建新插件实例
- 自动播放被反复重置
- 性能抖动、行为不可控
useRef 的本质价值:
在 React 渲染体系外,持久化一个可变对象。
这是一个非常典型、非常“面试级别”的用法。
六、交互细节:为什么鼠标移入要暂停?
onMouseEnter={() => plugin.current?.stop()}
onMouseLeave={() => plugin.current?.reset()}
这是一个看似很小,但体验影响极大的细节:
- 用户正在看内容
- 自动切走 = 强干扰
好的组件,不是功能多,而是尊重用户行为。
七、指示点设计:状态驱动,而不是 DOM 操作
slides.map((_, i) => (
<button
key={i}
className={`h-2 w-2 rounded-full transition-all
${selectedIndex === i ? 'bg-white w-6' : 'bg-white/50'}`}
/>
));
几个工程要点:
- 循环渲染,不操作 DOM
- 动态类名完全由状态驱动
transition-all提供平滑过渡
React 组件的本质:
UI = f(state)
八、CSS 与性能:渐变背景为什么优于图片?
bg-gradient-to-t from-black/60 to-transparent
相比图片背景,渐变的优势:
- 不需要额外 HTTP 请求
- 减少并发资源下载
- GPU 友好
- 更容易适配深浅色
在高性能场景下,
能不用图片,就不用图片。
九、总结:一个小组件,体现哪些工程能力?
回看这个 Carousel,你至少可以聊清楚:
- 组件拆分与职责边界
- 第三方库的正确使用方式
- React 状态与外部状态同步
useRef的真实应用场景- UI 细节与性能取舍
面试官真正想看的,不是你会不会写轮播图,
而是你为什么这么写。