使用 Next.js 16、TypeScript 和 Unsplash API 构建现代图片画廊

发布: (2025年12月12日 GMT+8 04:59)
5 min read
原文: Dev.to

Source: Dev.to

🛠 技术栈与特性

核心技术

  • Next.js 16 – App Router 与 Server Components
  • TypeScript – 完整的类型安全
  • Tailwind CSS v4 – 基于 CSS 变量的实用工具优先样式
  • Unsplash API – 高质量图片来源
  • Lucide React – 现代图标库

关键特性

  • ✅ 首屏渲染使用服务器端渲染 (SSR)
  • ✅ 热门照片使用静态站点生成 (SSG)
  • ✅ 动态路由 /photos/[id]
  • ✅ 使用 next/image 进行图片优化
  • ✅ 实时搜索并使用防抖
  • ✅ 分类过滤(自然、建筑等)
  • ✅ 根据系统偏好检测的暗黑模式
  • ✅ 响应式、移动优先设计
  • ✅ SEO 优化的动态元数据
  • ✅ 错误边界与加载状态

🏗 项目结构

文件目录

next-image-gallery/
├── app/
│   ├── layout.tsx           # Root layout with ThemeProvider
│   ├── page.tsx             # Home page (Server Component)
│   ├── loading.tsx          # Global loading UI
│   ├── error.tsx            # Global error boundary
│   ├── api/
│   │   └── photos/
│   │       └── route.ts     # API route for client‑side fetching
│   └── photos/[id]/
│       ├── page.tsx         # Dynamic photo detail page
│       ├── loading.tsx      # Photo detail loading state
│       └── not-found.tsx    # 404 page
├── components/
│   ├── ui/
│   │   └── Button.tsx       # Reusable button component
│   ├── ImageCard.tsx        # Photo card with hover effects
│   ├── ImageGrid.tsx        # Responsive grid layout
│   ├── ImageDetail.tsx      # Photo detail view
│   ├── Navigation.tsx       # Header navigation
│   ├── ThemeToggle.tsx      # Dark mode toggle (Client)
│   ├── SearchBar.tsx        # Search input with debounce (Client)
│   ├── FilterBar.tsx        # Category filters (Client)
│   └── GalleryClient.tsx    # Client‑side orchestrator
├── lib/
│   ├── api.ts               # Unsplash API functions
│   ├── types.ts             # TypeScript interfaces
│   └── utils.ts             # Helper functions
└── providers/
    └── ThemeProvider.tsx    # Theme context provider

⚡ Server Components 策略

Server 与 Client 组件

// app/page.tsx – Server Component (default)
export default async function Home() {
  // ✅ Fetch data on the server
  const initialPhotos = await getPhotos({
    page: 1,
    perPage: 20,
    orderBy: "popular",
  });

  return <GalleryClient initialPhotos={initialPhotos} />;
}

// components/GalleryClient.tsx – Client Component
"use client"; // ← Explicit directive

export function GalleryClient({ initialPhotos }: Props) {
  const [photos, setPhotos] = useState(initialPhotos);
  // ...interactive logic
}

为什么重要

Server Components

  • ✅ 不向客户端发送 JavaScript
  • ✅ 直接访问 API/数据库
  • ✅ 更好的 SEO(HTML 已包含内容)
  • ✅ 更快的首屏加载

Client Components

  • ✅ 交互性(hooks、事件处理)
  • ✅ 可访问浏览器 API(localStorage 等)
  • ✅ 实时更新

典型模式

Server Component (page.tsx)

在服务器上获取数据

传递给 Client Component

Client Component (GalleryClient.tsx)

处理用户交互

通过 API 路由获取额外数据

结果: 快速的 SSR 渲染 + 丰富的客户端交互,且保持最优的 bundle 大小。

🚀 动态路由与 SSG

为热门照片生成静态页面

// app/photos/[id]/page.tsx
export async function generateStaticParams() {
  const photos = await getPhotosForStaticGeneration(30);
  return photos.map((photo) => ({ id: photo.id }));
}

构建时输出

npm run build

# Example output:
 /photos/[id]                  (Static)
 /photos/abc123
 /photos/def456
 /photos/xyz789
... (30 total pages)

动态元数据用于 SEO

export async function generateMetadata({ params }: Props): Promise<any> {
  const { id } = await params;
  const photo = await getPhotoById(id);

  return {
    title: `${photo.alt_description} - Photo Gallery`,
    description: photo.description || `Photo by ${photo.user.name}`,
    openGraph: {
      images: [
        {
          url: photo.urls.regular,
          width: photo.width,
          height: photo.height,
        },
      ],
    },
    twitter: {
      card: "summary_large_image",
    },
  };
}

收益

  • 为每张照片生成唯一的 meta 标签,实现完美 SEO
  • 社交媒体预览效果美观
  • 预生成集合可实现瞬时页面加载

🖼 图片优化

Next.js Image 组件

(此处应插入 next/image 的使用示例,原文此节留空。)

关键优化点

  • 自动格式选择 – WebP、AVIF,回退到 JPEG/PNG
  • 响应式图片sizes="(max-width: 768px) 100vw, 33vw"
  • 懒加载 – 仅对前四张图片使用 priority={true}
  • 模糊占位 – 使用彩色 SVG 占位避免 CLS

配置

// next.config.ts
export default {
  images: {
    remotePatterns: [
      {
        protocol: "https",
        hostname: "images.unsplash.com",
        pathname: "/**",
      },
    ],
  },
};

🔍 搜索与过滤

防抖搜索组件

// components/SearchBar.tsx
export function SearchBar({ onSearch }: Props) {
  const [query, setQuery] = useState("");

  useEffect(() => {
    const debouncedSearch = debounce((value: string) => {
      onSearch(value);
    }, 500); // Wait 500 ms after user stops typing

    debouncedSearch(query);
  }, [query, onSearch]);

  return (
    <input
      value={query}
      onChange={(e) => setQuery(e.target.value)}
      placeholder="Search photos..."
    />
  );
}

为什么要防抖?
如果不进行防抖,每一次键入都会触发一次网络请求,这会给 API 带来不必要的负载,并导致用户体验下降。防抖可以把快速连续的输入合并为一次请求,在用户暂停后再发送。

所有代码片段均可直接复制到 Next.js 16 项目中使用。

Back to Blog

相关文章

阅读更多 »