小红书式两列布局?别被“随机高度”骗了!我的图片差点叠成俄罗斯方块
🌊 开头:为什么我的瀑布流像“地震后的砖墙”?
那天,我信心满满地写了个瀑布流组件,心想:“不就是把图片分两列嘛,i % 2 一搞,完事!”
结果一跑起来——左边一列堆得像埃菲尔铁塔,右边矮得像乐高积木。用户往下滚,新图“啪”一下插进左边,整个页面抖得像在蹦迪。
设计师路过看了一眼,幽幽地说:“你这叫‘瀑布’?我看是‘泥石流’。”
我脸一红,决定痛改前非。
🧱 第一步:先让图片“站好队”——两列布局怎么搞?
很多人以为瀑布流要用 Masonry 库,其实纯 CSS + React 就够了!
我的方案超简单:
- 所有图片存在一个数组
images - 偶数索引(0,2,4…)→ 左列
- 奇数索引(1,3,5…)→ 右列
<div className={styles.column}>
{images.filter((_, i) => i % 2 === 0).map(img => (
<ImageCard key={img.id} {...img} />
))}
</div>
<div className={styles.column}>
{images.filter((_, i) => i % 2 === 1).map(img => (
<ImageCard key={img.id} {...img} />
))}
</div>
CSS 更简单:
.wrapper {
display: flex;
justify-content: space-between;
padding: 16px;
}
.column {
width: 48%;
margin: 0 1%;
display: flex;
flex-direction: column;
}
💡 为什么不用“哪列矮塞哪列”?
因为第一版先做对,再做好!奇偶拆分虽然不完美,但代码清晰、性能稳定,面试官一看就懂。
🔍 第二步:滚动到底?自动加载更多!
用户刷到快到底了,总不能让他手动点“加载更多”吧?那多 low!
于是,我祭出 IntersectionObserver ——浏览器原生的“盯梢高手”。
我在底部放了个“卧底”元素:
<div ref={loader} className={styles.loader}>加载中...</div>
然后派一个“观察者”盯着它:
useEffect(() => {
const observer = new IntersectionObserver(([entry]) => {
if (entry.isIntersecting) {
fetchMore(); // 去要下一页数据!
}
observer.unobserve(entry.target); // 看一眼就撤,避免重复触发
});
if (loader.current) observer.observe(loader.current);
return () => observer.disconnect(); // 组件卸载时,彻底解散观察者
}, []);
✅ 为什么不用
scroll事件?
因为scroll会疯狂触发,性能差;而IntersectionObserver是浏览器优化过的,只在元素进出视窗时通知你,省电又安静。
🧹 第三步:别忘了“打扫战场”——内存泄漏警告!
很多教程只教你怎么“观察”,却不说怎么“收摊”。
但我知道:前端工程师的修养,藏在 disconnect() 里。
所以我在 useEffect 的清理函数里加了:
return () => observer.disconnect();
否则,用户切到其他页面,这个观察者还在后台偷偷运行——就像你出门忘了关煤气,迟早出事。
🎯 结尾:瀑布流稳了,但还能更好!
现在,我的瀑布流:
- 图片分两列,不打架
- 滚到底自动加载,不打断浏览
- 内存安全,不拖累性能
但它还有提升空间:
- 图片懒加载(先占位,滑到才加载)
- 骨架屏(加载时显示灰色块,不白屏)
- 动态列高分配(真正的小红书体验)
这些,留到下次升级再说。毕竟,旅行不是冲刺,而是慢慢走,稳稳看风景。
下一站,我会带你搞定“搜索框”——如何让用户打字不卡顿?如何缓存历史记录?防抖 + useMemo + localStorage,一套组合拳打穿性能瓶颈!