How does Draftboard implement masonry layout?

HEAD
d872dbdShow video first-frame thumbnails in feed and grid views (#18)
This post might have stale content, as HEAD is 3 commits ahead.
Avatar of WhoKnowInfinity
Infinity
posted

详细解释这个库是如何实现在主页实现 masonry layout 的

Avatar of anthropic/claude-haiku-4.5
Claude Haiku 4.5
commented

完美!现在让我创建一个详细的文档来解释整个实现流程:

##详细解析: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 键

核心功能:

  1. 视图模式管理

    • 使用 React 状态管理当前视图模式
    • 通过 localStorage 持久化用户的选择
  2. 数据获取

    api.post.feed.useInfiniteQuery(
      { limit: 10 },
      { getNextPageParam: (lastPage) => lastPage.nextCursor }
    )
    
    • 使用 tRPC 的无限查询获取文章列表
    • 每次加载 10 条文章
    • 支持光标分页(cursor-based pagination)
  3. 无限滚动实现

    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,
    });
    
  4. 视图切换界面

    <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-cssMasonry 布局库
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️⃣ 关键优化

  1. 缓存签名 URL

    staleTime: 30 * 60 * 1000,  // 30 分钟缓存
    
  2. 禁用不必要的重新获取

    refetchOnWindowFocus: false,
    
  3. 延迟加载触发

    rootMargin: "100px"  // 在用户到达底部前 100px 时就开始加载
    
  4. 条件查询

    enabled: !!r2Key,  // 仅在有有效密钥时查询
    

###8️⃣ 使用流程总结

  1. 用户访问首页 (/app/(main)/page.tsx)
  2. FeedView 加载,默认显示 Feed 视图(或从 localStorage 恢复上次选择)
  3. 用户点击 Grid 按钮 → 切换到 GridView
  4. GridView 使用 Masonry 库进行响应式布局
  5. 文章图片通过 R2 签名 URL 安全加载
  6. 用户滚动时,IntersectionObserver 触发无限加载
  7. 新文章自动填充到 Masonry 网格中

这就是 Draftboard 实现 Masonry 布局的完整解决方案!🎨


END OF POST

How does Draftboard implement masonry layout? — hrescak/Draftboard