How does Draftboard implement masonry layout?
This post might have stale content, as HEAD is 3 commits ahead.
详细解释这个库是如何实现在主页实现 masonry layout 的
完美!现在让我创建一个详细的文档来解释整个实现流程:
##详细解析:Draftboard 的 Masonry Layout 实现
###📋 总体流程
Draftboard 使用 React Masonry CSS 库来实现主页的瀑布流(Masonry)布局。用户可以通过切换按钮在 Feed 视图和 Grid 视图之间切换。
###1️⃣ 核心架构
首页 (page.tsx)
↓
FeedView 组件(feed/FeedView.tsx)
├── 提供视图切换功能(Feed ↔ Grid)
├── 管理数据加载(使用 tRPC 无限查询)
└── 根据 viewMode 状态选择渲染
├─ Feed 模式 → PostCard 组件列表
└─ Grid 模式 → GridView 组件 (Masonry)
###2️⃣ 关键组件详解
####A. FeedView 组件 (src/components/feed/FeedView.tsx)
type ViewMode = "feed" | "grid";
const VIEW_MODE_KEY = "draftboard-view-mode"; // localStorage 键
核心功能:
-
视图模式管理
- 使用 React 状态管理当前视图模式
- 通过
localStorage持久化用户的选择
-
数据获取
api.post.feed.useInfiniteQuery( { limit: 10 }, { getNextPageParam: (lastPage) => lastPage.nextCursor } )- 使用 tRPC 的无限查询获取文章列表
- 每次加载 10 条文章
- 支持光标分页(cursor-based pagination)
-
无限滚动实现
const handleObserver = useCallback( (entries: IntersectionObserverEntry[]) => { const [target] = entries; if (target?.isIntersecting && hasNextPage && !isFetchingNextPage) { fetchNextPage(); } }, [fetchNextPage, hasNextPage, isFetchingNextPage] ); // IntersectionObserver 配置 const observer = new IntersectionObserver(handleObserver, { rootMargin: "100px", // 距离底部 100px 时触发加载 threshold: 0, }); -
视图切换界面
<Tabs value={viewMode} onValueChange={(v) => handleViewModeChange(v as ViewMode)}> <TabsList> <TabsTrigger value="feed"><List /> Feed</TabsTrigger> <TabsTrigger value="grid"><LayoutGrid /> Grid</TabsTrigger> </TabsList> </Tabs>
####B. GridView 组件 (src/components/feed/GridView.tsx) - 核心 Masonry 实现
依赖库:
import Masonry from "react-masonry-css";
关键配置:
const masonryBreakpoints = {
default: 5, // 桌面端: 5 列
1280: 4, // 宽: 4 列
1024: 3, // 平板: 3 列
640: 2, // 手机: 2 列
};
这是 响应式设计 的关键配置,在不同屏幕宽度下显示不同的列数。
Masonry 组件用法:
<Masonry
breakpointCols={masonryBreakpoints} // 设置响应式列数
className="flex w-auto -ml-3" // 主容器 CSS
columnClassName="pl-3 bg-clip-padding" // 列的 CSS
>
{visualPosts.map(({ post, attachment }) => (
<Link href={`/post/${post.id}`} className="group mb-3 block overflow-hidden rounded-lg">
{/* 内容 */}
</Link>
))}
</Masonry>
内部工作原理:
react-masonry-css使用 CSS 多列布局(CSS Columns)- 通过
breakpointCols定义响应式列数 - 在不同断点自动调整列数
- 元素会自动填充到各列中
图片/视频处理:
// 筛选包含图片或视频的文章
const visualPosts = posts
.map((post) => {
const firstVisual = post.attachments.find(
(att) => att.type === "IMAGE" || att.type === "VIDEO"
);
return firstVisual ? { post, attachment: firstVisual } : null;
})
.filter((item): item is { post: Post; attachment: Post["attachments"][0] } => item !== null);
有趣的细节 - 签名 URL 处理:
由于项目使用 Cloudflare R2 存储,所需获取签名 URL:
function SignedImage({ url, alt, className }: { url: string; alt: string; className?: string }) {
const r2Key = extractR2Key(url); // 从 URL 提取 R2 密钥
const { data: signedUrlData, isLoading } = api.upload.getDownloadUrl.useQuery(
{ key: r2Key! },
{
enabled: !!r2Key,
staleTime: 30 * 60 * 1000, // 缓存 30 分钟
refetchOnWindowFocus: false,
}
);
return <img src={signedUrlData?.url || url} alt={alt} />;
}
视频缩略图处理:
<video
src={`${videoSrc}#t=0.001`} // 提取第 0.001 秒的帧作为缩略图
muted
preload="metadata"
>
<track kind="captions" />
</video>
hover 效果:
className="w-full transition-transform group-hover:scale-105"
在 Tailwind CSS 中使用 group-hover:scale-105,实现平滑的缩放动画。
###3️⃣ CSS 核心细节
// Masonry 容器
className="flex w-auto -ml-3"
// flex: 使用 flexbox
// w-auto: 宽度自动
// -ml-3: 负外边距(配合列的 pl-3 形成网格间距)
// 列的样式
columnClassName="pl-3 bg-clip-padding"
// pl-3: 左内边距(创建列间距)
// bg-clip-padding: 背景裁剪到内边距边界
###4️⃣ 响应式流程
用户改变窗口大小
↓
Masonry 检测到断点变化
↓
重新计算列数(使用 breakpointCols 映射)
↓
自动重排元素到新的列中
例如:
- 1440px+ (桌面) → 5 列
- 1280-1440px → 4 列
- 1024-1280px → 3 列
- 640-1024px (平板) → 2 列
- <640px (手机) → 2 列
###5️⃣ 技术栈总结
| 技术 | 用途 |
|---|---|
react-masonry-css | Masonry 布局库 |
Intersection Observer API | 无限滚动触发 |
localStorage | 保存视图模式偏好 |
tRPC | 数据获取 |
Next.js Image | 不在代码中使用,用原生 <img> |
Tailwind CSS | 样式和响应式设计 |
###6️⃣ 数据流图
FeedView 初始化
↓
useInfiniteQuery → 获取第一页 10 条文章
↓
GridView 接收 posts 数据
↓
过滤出有视觉内容的文章 (IMAGE/VIDEO)
↓
为每个文章生成签名 URL (tRPC 查询)
↓
Masonry 布局渲染
↓
用户滚动到底部 (100px 范围内)
↓
IntersectionObserver 触发 → fetchNextPage()
↓
加载下一页 10 条文章
↓
重新渲染 Masonry 布局
###7️⃣ 关键优化
-
缓存签名 URL
staleTime: 30 * 60 * 1000, // 30 分钟缓存 -
禁用不必要的重新获取
refetchOnWindowFocus: false, -
延迟加载触发
rootMargin: "100px" // 在用户到达底部前 100px 时就开始加载 -
条件查询
enabled: !!r2Key, // 仅在有有效密钥时查询
###8️⃣ 使用流程总结
- 用户访问首页 (
/app/(main)/page.tsx) FeedView加载,默认显示 Feed 视图(或从 localStorage 恢复上次选择)- 用户点击 Grid 按钮 → 切换到
GridView GridView使用 Masonry 库进行响应式布局- 文章图片通过 R2 签名 URL 安全加载
- 用户滚动时,IntersectionObserver 触发无限加载
- 新文章自动填充到 Masonry 网格中
这就是 Draftboard 实现 Masonry 布局的完整解决方案!🎨
END OF POST