Supabase图片处理 #

一、图片处理概述 #

1.1 内置图片处理 #

text
Supabase图片处理功能
├── 图片缩放
├── 图片裁剪
├── 格式转换
├── 质量调整
├── 水印添加
└── 自动优化

1.2 处理方式 #

text
图片处理方式
├── URL参数处理
│   └── 在URL中添加参数
│
└── 实时处理
    └── 请求时即时处理

二、基础图片转换 #

2.1 获取图片URL #

typescript
// 获取原始图片URL
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg')

// 原始URL
// https://xxx.supabase.co/storage/v1/object/public/images/photo.jpg

2.2 图片缩放 #

typescript
// 使用transform选项
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 500,
      height: 300,
    }
  })

// 生成的URL包含转换参数
// https://xxx.supabase.co/storage/v1/render/image/public/images/photo.jpg?width=500&height=300

2.3 调整质量 #

typescript
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      quality: 80, // 1-100
    }
  })

2.4 格式转换 #

typescript
// 转换为WebP格式
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      format: 'webp',
      quality: 80,
    }
  })

// 支持的格式
// - origin: 保持原格式
// - avif: AVIF格式
// - webp: WebP格式
// - jpg: JPEG格式
// - png: PNG格式

三、缩放模式 #

3.1 按宽度缩放 #

typescript
// 固定宽度,高度自动
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
    }
  })

3.2 按高度缩放 #

typescript
// 固定高度,宽度自动
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      height: 600,
    }
  })

3.3 固定尺寸 #

typescript
// 固定宽高
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 400,
      height: 300,
      resize: 'cover', // 覆盖模式
    }
  })

3.4 缩放模式 #

typescript
// cover: 覆盖,可能裁剪
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 400,
      height: 300,
      resize: 'cover',
    }
  })

// contain: 包含,保持比例
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 400,
      height: 300,
      resize: 'contain',
    }
  })

// fill: 填充,可能变形
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 400,
      height: 300,
      resize: 'fill',
    }
  })

四、图片裁剪 #

4.1 基础裁剪 #

typescript
// 指定裁剪区域
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 200,
      height: 200,
      resize: 'cover',
    }
  })

五、图片组件 #

5.1 响应式图片组件 #

tsx
import { supabase } from '../lib/supabase'

interface ResponsiveImageProps {
  bucket: string
  path: string
  alt: string
  sizes?: {
    thumbnail?: number
    small?: number
    medium?: number
    large?: number
  }
}

export function ResponsiveImage({ 
  bucket, 
  path, 
  alt,
  sizes = {
    thumbnail: 150,
    small: 300,
    medium: 600,
    large: 1200,
  }
}: ResponsiveImageProps) {
  const baseUrl = supabase.storage.from(bucket).getPublicUrl(path).data.publicUrl

  const srcSet = Object.entries(sizes)
    .map(([size, width]) => {
      const url = supabase.storage
        .from(bucket)
        .getPublicUrl(path, {
          transform: { width }
        }).data.publicUrl
      return `${url} ${width}w`
    })
    .join(', ')

  return (
    <img
      src={baseUrl}
      srcSet={srcSet}
      sizes="(max-width: 600px) 300px, (max-width: 1200px) 600px, 1200px"
      alt={alt}
      loading="lazy"
    />
  )
}

5.2 头像组件 #

tsx
import { supabase } from '../lib/supabase'

interface AvatarProps {
  userId: string
  size?: 'sm' | 'md' | 'lg'
}

export function Avatar({ userId, size = 'md' }: AvatarProps) {
  const sizes = {
    sm: 32,
    md: 48,
    lg: 96,
  }

  const { data } = supabase.storage
    .from('avatars')
    .getPublicUrl(`${userId}/avatar.jpg`, {
      transform: {
        width: sizes[size],
        height: sizes[size],
        resize: 'cover',
      }
    })

  return (
    <img
      src={data.publicUrl}
      alt="Avatar"
      width={sizes[size]}
      height={sizes[size]}
      className="rounded-full"
    />
  )
}

5.3 图片画廊组件 #

tsx
import { useState } from 'react'
import { supabase } from '../lib/supabase'

interface GalleryProps {
  bucket: string
  images: string[]
}

export function Gallery({ bucket, images }: GalleryProps) {
  const [selected, setSelected] = useState<string | null>(null)

  return (
    <div>
      <div className="grid grid-cols-4 gap-4">
        {images.map((path) => (
          <img
            key={path}
            src={supabase.storage
              .from(bucket)
              .getPublicUrl(path, {
                transform: { width: 200, height: 200, resize: 'cover' }
              }).data.publicUrl}
            alt=""
            onClick={() => setSelected(path)}
            className="cursor-pointer"
          />
        ))}
      </div>

      {selected && (
        <div className="fixed inset-0 bg-black/80 flex items-center justify-center">
          <img
            src={supabase.storage
              .from(bucket)
              .getPublicUrl(selected, {
                transform: { width: 1200 }
              }).data.publicUrl}
            alt=""
          />
          <button 
            onClick={() => setSelected(null)}
            className="absolute top-4 right-4 text-white"
          >
            Close
          </button>
        </div>
      )}
    </div>
  )
}

六、图片上传优化 #

6.1 上传前压缩 #

typescript
async function compressImage(
  file: File,
  maxWidth: number = 1920,
  quality: number = 0.8
): Promise<Blob> {
  return new Promise((resolve) => {
    const img = new Image()
    img.src = URL.createObjectURL(file)
    
    img.onload = () => {
      const canvas = document.createElement('canvas')
      let { width, height } = img
      
      if (width > maxWidth) {
        height = (height * maxWidth) / width
        width = maxWidth
      }
      
      canvas.width = width
      canvas.height = height
      
      const ctx = canvas.getContext('2d')!
      ctx.drawImage(img, 0, 0, width, height)
      
      canvas.toBlob(
        (blob) => resolve(blob!),
        'image/jpeg',
        quality
      )
      
      URL.revokeObjectURL(img.src)
    }
  })
}

// 使用
const compressed = await compressImage(file, 1920, 0.8)
await supabase.storage.from('images').upload(path, compressed)

6.2 生成缩略图 #

typescript
async function generateThumbnail(
  file: File,
  size: number = 200
): Promise<Blob> {
  return new Promise((resolve) => {
    const img = new Image()
    img.src = URL.createObjectURL(file)
    
    img.onload = () => {
      const canvas = document.createElement('canvas')
      canvas.width = size
      canvas.height = size
      
      const ctx = canvas.getContext('2d')!
      
      // 居中裁剪
      const minDim = Math.min(img.width, img.height)
      const sx = (img.width - minDim) / 2
      const sy = (img.height - minDim) / 2
      
      ctx.drawImage(img, sx, sy, minDim, minDim, 0, 0, size, size)
      
      canvas.toBlob(
        (blob) => resolve(blob!),
        'image/jpeg',
        0.8
      )
      
      URL.revokeObjectURL(img.src)
    }
  })
}

七、图片处理最佳实践 #

7.1 性能优化 #

text
图片优化建议
├── 使用WebP格式
├── 适当降低质量(70-80%)
├── 按需生成尺寸
├── 使用CDN缓存
├── 懒加载图片
└── 使用响应式图片

7.2 缓存策略 #

typescript
// 上传时设置缓存
const { data, error } = await supabase.storage
  .from('images')
  .upload(path, file, {
    cacheControl: '31536000', // 1年
  })

// 转换图片也会被缓存
const { data } = supabase.storage
  .from('images')
  .getPublicUrl('photo.jpg', {
    transform: {
      width: 800,
      quality: 80,
    }
  })

八、总结 #

图片处理要点:

操作 参数
缩放 width, height
质量调整 quality (1-100)
格式转换 format (webp, avif, jpg, png)
缩放模式 resize (cover, contain, fill)

下一步,让我们学习实时订阅!

最后更新:2026-03-28