Chuyển đến nội dung chính

第 16 課:文件上傳和圖像優化

使用伺服器操作上傳檔案。 next/image 元件,響應式影像。雲端儲存(S3、Cloudinary)。影像優化策略。

💻 程式設計 — 第 16 課 第 16 課:文件上傳和圖像優化

React 和 Next.js:從基礎到高級

第 5 部分:全端功能

亞洲開發網

1. 使用伺服器操作上傳文件

// actions/upload.ts
'use server';
import { writeFile, mkdir } from 'fs/promises';
import path from 'path';
import crypto from 'crypto';

export async function uploadFile(formData: FormData) {
  const file = formData.get('file') as File;
  if (!file || file.size === 0) {
    return { error: 'Chưa chọn file' };
  }

  // Validate file type
  const allowedTypes = ['image/jpeg', 'image/png', 'image/webp'];
  if (!allowedTypes.includes(file.type)) {
    return { error: 'Chỉ cho phép JPEG, PNG, WebP' };
  }

  // Validate size (max 5MB)
  if (file.size > 5 * 1024 * 1024) {
    return { error: 'File tối đa 5MB' };
  }

  const bytes = await file.arrayBuffer();
  const buffer = Buffer.from(bytes);

  // Generate unique filename
  const ext = path.extname(file.name);
  const filename = `${crypto.randomUUID()}${ext}`;
  const uploadDir = path.join(process.cwd(), 'public/uploads');

  await mkdir(uploadDir, { recursive: true });
  await writeFile(path.join(uploadDir, filename), buffer);

  return { url: `/uploads/${filename}` };
}

2.上傳表單元件

'use client';
import { useState, useRef } from 'react';
import { uploadFile } from '@/actions/upload';

export function UploadForm() {
  const [preview, setPreview] = useState<string | null>(null);
  const [uploading, setUploading] = useState(false);
  const inputRef = useRef<HTMLInputElement>(null);

  const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
    const file = e.target.files?.[0];
    if (file) {
      setPreview(URL.createObjectURL(file));
    }
  };

  const handleSubmit = async (formData: FormData) => {
    setUploading(true);
    const result = await uploadFile(formData);
    setUploading(false);

    if (result.error) {
      alert(result.error);
    } else {
      alert(`Uploaded: ${result.url}`);
    }
  };

  return (
    <form action={handleSubmit}>
      <input
        ref={inputRef}
        type="file"
        name="file"
        accept="image/*"
        onChange={handleChange}
      />
      {preview && <img src={preview} alt="Preview" width={200} />}
      <button type="submit" disabled={uploading}>
        {uploading ? 'Đang upload...' : 'Upload'}
      </button>
    </form>
  );
}

3. next/image 元件

import Image from 'next/image';

// Local image — tự động optimize
<Image
  src="/images/hero.jpg"
  alt="Hero"
  width={1200}
  height={630}
  priority // Preload for LCP image
/>

// Remote image — cần config domain
<Image
  src="https://cdn.example.com/photo.jpg"
  alt="Photo"
  width={800}
  height={600}
  quality={80}
  placeholder="blur"
  blurDataURL="data:image/..."
/>

// Fill mode — responsive container
<div className="relative w-full h-64">
  <Image
    src="/images/banner.jpg"
    alt="Banner"
    fill
    sizes="(max-width: 768px) 100vw, 50vw"
    className="object-cover"
  />
</div>

4. 圖片配置

// next.config.ts
import type { NextConfig } from 'next';

const nextConfig: NextConfig = {
  images: {
    remotePatterns: [
      {
        protocol: 'https',
        hostname: 'cdn.example.com',
      },
      {
        protocol: 'https',
        hostname: '*.amazonaws.com',
      },
    ],
    formats: ['image/avif', 'image/webp'],
    deviceSizes: [640, 750, 828, 1080, 1200, 1920],
  },
};

export default nextConfig;

5.上傳到S3

// lib/s3.ts
import { S3Client, PutObjectCommand } from '@aws-sdk/client-s3';
import { getSignedUrl } from '@aws-sdk/s3-request-presigner';

const s3 = new S3Client({
  region: process.env.AWS_REGION!,
  credentials: {
    accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
    secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!,
  },
});

export async function getPresignedUrl(filename: string, contentType: string) {
  const key = `uploads/${Date.now()}-${filename}`;
  const command = new PutObjectCommand({
    Bucket: process.env.S3_BUCKET!,
    Key: key,
    ContentType: contentType,
  });

  const url = await getSignedUrl(s3, command, { expiresIn: 600 });
  return { url, key };
}

下一篇: 實時和 WebSocket ——即時互動。