08. 静态资源管理

Next.js 提供了强大的静态资源管理功能,包括图片优化、字体加载、public 目录等。本章将深入学习这些功能。


1. public 目录

public 目录用于存放静态资源,这些文件可以直接通过 URL 访问。

目录结构

public/
├── images/
│   ├── logo.png
│   └── hero.jpg
├── icons/
│   └── favicon.ico
└── files/
    └── resume.pdf

访问方式

// 在组件中使用
<img src="/images/logo.png" alt="Logo" />
<Link href="/files/resume.pdf">下载简历</Link>

⚠️ 注意public 目录中的文件路径以 / 开头,不需要包含 public


2. Next.js Image 组件

Next.js 提供了优化的 Image 组件,自动处理图片优化、懒加载、响应式等。

基础用法

// src/app/page.tsx
import Image from 'next/image'
 
export default function Home() {
  return (
    <div>
      <Image
        src="/images/hero.jpg"
        alt="Hero image"
        width={800}
        height={600}
      />
    </div>
  )
}

为什么使用 Image 组件?

  • 自动优化:WebP、AVIF 格式转换
  • 懒加载:只加载可见区域的图片
  • 响应式:根据设备自动调整尺寸
  • 防止布局偏移:自动计算宽高比

重要属性

<Image
  src="/images/photo.jpg"
  alt="描述文字"
  width={800}        // 必需:图片宽度
  height={600}       // 必需:图片高度
  priority={false}   // 是否优先加载(首屏图片设为 true)
  quality={75}       // 图片质量(1-100,默认 75)
  placeholder="blur" // 占位符类型
  blurDataURL="..."  // 模糊占位符数据
  fill={false}       // 是否填充父容器
  sizes="(max-width: 768px) 100vw, 50vw" // 响应式尺寸
/>

3. 实战案例:响应式图片

固定尺寸图片

// src/components/HeroImage.tsx
import Image from 'next/image'
 
export default function HeroImage() {
  return (
    <div className="relative w-full h-96">
      <Image
        src="/images/hero.jpg"
        alt="Hero"
        fill
        className="object-cover"
        priority
      />
    </div>
  )
}

响应式图片

// src/components/ProductImage.tsx
import Image from 'next/image'
 
interface ProductImageProps {
  src: string
  alt: string
}
 
export default function ProductImage({ src, alt }: ProductImageProps) {
  return (
    <div className="relative w-full aspect-square">
      <Image
        src={src}
        alt={alt}
        fill
        className="object-cover rounded-lg"
        sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
      />
    </div>
  )
}

使用 blur 占位符

import Image from 'next/image'
 
const blurDataURL = 'data:image/jpeg;base64,/9j/4AAQSkZJRg...' // 小尺寸 base64 图片
 
export default function BlurImage() {
  return (
    <Image
      src="/images/large.jpg"
      alt="Large image"
      width={1200}
      height={800}
      placeholder="blur"
      blurDataURL={blurDataURL}
    />
  )
}

4. 外部图片(CDN)

配置允许的域名

// next.config.js
module.exports = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'example.com',
        pathname: '/images/**',
      },
      {
        protocol: 'https',
        hostname: '**.cloudinary.com', // 支持通配符
      },
    ],
  },
}

使用外部图片

import Image from 'next/image'
 
export default function ExternalImage() {
  return (
    <Image
      src="https://example.com/images/photo.jpg"
      alt="External image"
      width={800}
      height={600}
    />
  )
}

5. 字体优化

Next.js 提供了 next/font 用于优化字体加载。

使用 Google Fonts

// src/app/layout.tsx
import { Inter } from 'next/font/google'
 
const inter = Inter({ subsets: ['latin'] })
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN" className={inter.className}>
      <body>{children}</body>
    </html>
  )
}

使用本地字体

// src/app/layout.tsx
import localFont from 'next/font/local'
 
const myFont = localFont({
  src: [
    {
      path: '../fonts/CustomFont-Regular.woff2',
      weight: '400',
      style: 'normal',
    },
    {
      path: '../fonts/CustomFont-Bold.woff2',
      weight: '700',
      style: 'normal',
    },
  ],
  variable: '--font-custom',
})
 
export default function RootLayout({
  children,
}: {
  children: React.ReactNode
}) {
  return (
    <html lang="zh-CN" className={myFont.variable}>
      <body>{children}</body>
    </html>
  )
}

在 CSS 中使用字体变量

/* src/app/globals.css */
:root {
  --font-custom: 'CustomFont', sans-serif;
}
 
.custom-text {
  font-family: var(--font-custom);
}

6. 图标和 Favicon

使用 Next.js App Icon

app 目录下放置 icon.pngicon.ico,Next.js 会自动生成 favicon:

app/
├── icon.png      # 自动生成 favicon
├── apple-icon.png # iOS 图标
└── layout.tsx

手动配置

// src/app/layout.tsx
export const metadata = {
  icons: {
    icon: '/icons/favicon.ico',
    apple: '/icons/apple-icon.png',
  },
}

7. 文件下载

提供文件下载

// src/app/download/page.tsx
import Link from 'next/link'
 
export default function DownloadPage() {
  return (
    <div>
      <h1>下载文件</h1>
      <Link
        href="/files/resume.pdf"
        download
        className="bg-blue-600 text-white px-6 py-3 rounded-lg"
      >
        下载简历
      </Link>
    </div>
  )
}

通过 API 路由提供文件

// src/app/api/download/route.ts
import { NextResponse } from 'next/server'
import fs from 'fs'
import path from 'path'
 
export async function GET() {
  const filePath = path.join(process.cwd(), 'public', 'files', 'resume.pdf')
  const fileBuffer = fs.readFileSync(filePath)
 
  return new NextResponse(fileBuffer, {
    headers: {
      'Content-Type': 'application/pdf',
      'Content-Disposition': 'attachment; filename="resume.pdf"',
    },
  })
}

8. 实战案例:图片画廊

需求

创建一个图片画廊,展示多张图片,支持响应式和懒加载。

实现

// src/app/gallery/page.tsx
import Image from 'next/image'
 
const images = [
  { src: '/images/gallery/1.jpg', alt: '图片 1' },
  { src: '/images/gallery/2.jpg', alt: '图片 2' },
  { src: '/images/gallery/3.jpg', alt: '图片 3' },
  { src: '/images/gallery/4.jpg', alt: '图片 4' },
  { src: '/images/gallery/5.jpg', alt: '图片 5' },
  { src: '/images/gallery/6.jpg', alt: '图片 6' },
]
 
export default function GalleryPage() {
  return (
    <div className="min-h-screen py-20">
      <div className="max-w-7xl mx-auto px-4">
        <h1 className="text-4xl font-bold mb-12 text-center">图片画廊</h1>
        <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
          {images.map((image, index) => (
            <div
              key={index}
              className="relative aspect-square overflow-hidden rounded-lg shadow-md hover:shadow-xl transition-shadow"
            >
              <Image
                src={image.src}
                alt={image.alt}
                fill
                className="object-cover"
                sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
              />
            </div>
          ))}
        </div>
      </div>
    </div>
  )
}

9. 实战案例:头像组件

使用 Next.js Image 优化头像

// src/components/Avatar.tsx
import Image from 'next/image'
 
interface AvatarProps {
  src: string
  alt: string
  size?: number
}
 
export default function Avatar({ src, alt, size = 64 }: AvatarProps) {
  return (
    <div
      className="relative rounded-full overflow-hidden"
      style={{ width: size, height: size }}
    >
      <Image
        src={src}
        alt={alt}
        fill
        className="object-cover"
        sizes={`${size}px`}
      />
    </div>
  )
}

使用示例

// src/app/profile/page.tsx
import Avatar from '@/components/Avatar'
 
export default function ProfilePage() {
  return (
    <div className="max-w-4xl mx-auto px-4 py-20">
      <div className="flex items-center gap-6">
        <Avatar src="/images/avatar.jpg" alt="用户头像" size={128} />
        <div>
          <h1 className="text-3xl font-bold">用户名</h1>
          <p className="text-gray-600">用户简介</p>
        </div>
      </div>
    </div>
  )
}

10. 性能优化技巧

1. 使用 priority 属性

首屏可见的图片应该设置 priority={true}

<Image
  src="/images/hero.jpg"
  alt="Hero"
  width={1200}
  height={600}
  priority // 优先加载
/>

2. 使用 sizes 属性

告诉浏览器在不同屏幕尺寸下图片的显示大小:

<Image
  src="/images/product.jpg"
  alt="Product"
  width={800}
  height={600}
  sizes="(max-width: 768px) 100vw, (max-width: 1200px) 50vw, 33vw"
/>

3. 使用 blur 占位符

提升用户体验,避免布局偏移:

<Image
  src="/images/large.jpg"
  alt="Large"
  width={1200}
  height={800}
  placeholder="blur"
  blurDataURL={blurDataURL}
/>

11. 常见问题

问题 1:图片不显示

原因:路径错误或图片不在 public 目录

解决

  • 确保图片在 public 目录下
  • 使用 /images/photo.jpg 而不是 /public/images/photo.jpg

问题 2:外部图片报错

原因:未在 next.config.js 中配置允许的域名

解决:在 next.config.js 中添加 remotePatterns

问题 3:图片尺寸警告

原因:未指定 widthheight,或使用 fill 时父容器未设置 position: relative

解决

  • 指定明确的宽高,或
  • 使用 fill 时确保父容器有 relative 定位

12. 实战练习:构建产品展示页

需求

创建一个产品展示页,包含:

  • 产品主图(使用 Image 组件)
  • 产品缩略图列表
  • 响应式设计

实现

// src/app/products/[id]/page.tsx
'use client'
 
import { useState } from 'react'
import Image from 'next/image'
import { useParams } from 'next/navigation'
 
const productImages = [
  '/images/products/1/main.jpg',
  '/images/products/1/thumb1.jpg',
  '/images/products/1/thumb2.jpg',
  '/images/products/1/thumb3.jpg',
]
 
export default function ProductPage() {
  const params = useParams()
  const [selectedImage, setSelectedImage] = useState(productImages[0])
 
  return (
    <div className="max-w-6xl mx-auto px-4 py-20">
      <div className="grid md:grid-cols-2 gap-8">
        {/* 主图 */}
        <div>
          <div className="relative aspect-square mb-4">
            <Image
              src={selectedImage}
              alt="产品主图"
              fill
              className="object-cover rounded-lg"
              priority
            />
          </div>
          
          {/* 缩略图 */}
          <div className="grid grid-cols-4 gap-2">
            {productImages.map((image, index) => (
              <button
                key={index}
                onClick={() => setSelectedImage(image)}
                className={`relative aspect-square rounded overflow-hidden border-2 ${
                  selectedImage === image
                    ? 'border-blue-600'
                    : 'border-transparent'
                }`}
              >
                <Image
                  src={image}
                  alt={`缩略图 ${index + 1}`}
                  fill
                  className="object-cover"
                />
              </button>
            ))}
          </div>
        </div>
 
        {/* 产品信息 */}
        <div>
          <h1 className="text-4xl font-bold mb-4">产品名称</h1>
          <p className="text-3xl font-bold text-blue-600 mb-6">¥999</p>
          <p className="text-gray-700 mb-8">
            这是产品的详细描述信息...
          </p>
          <button className="w-full bg-blue-600 text-white py-3 rounded-lg hover:bg-blue-700">
            加入购物车
          </button>
        </div>
      </div>
    </div>
  )
}

13. 总结

本章我们学习了:

  1. public 目录的使用
  2. ✅ Next.js Image 组件的优化功能
  3. ✅ 字体优化(next/font
  4. ✅ 图标和 Favicon 配置
  5. ✅ 文件下载的实现
  6. ✅ 构建了图片画廊和产品展示页

关键要点

  • 🖼️ 始终使用 Image 组件而不是 <img> 标签
  • 🖼️ 首屏图片设置 priority={true}
  • 🖼️ 使用 sizes 属性优化响应式图片
  • 🖼️ 外部图片需要在 next.config.js 中配置

下一步:恭喜!你已经完成了 Next.js 基础入门的学习。接下来可以深入学习核心机制、App Router 体系等高级内容。