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

Bài 16: File Upload & Image Optimization

File upload với Server Actions. next/image component, responsive images. Cloud storage (S3, Cloudinary). Image optimization strategies.

💻 Lập trình — Bài 16 Bài 16: File Upload & Image Optimization

React & Next.js: Từ Cơ bản đến Nâng cao

Phần 5: Full-Stack Features

xdev.asia

1. File Upload với Server Action

// 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. Upload Form Component

'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 Component

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. Image Config

// 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. Upload lên 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 };
}

Bài tiếp theo: Real-time & WebSockets — tương tác thời gian thực.