Гайды

Как добавить загрузку картинок в приложение — 5 шагов и одна ловушка

Иллюстрация: картинка едет в отдельный склад, а в картотеку ложится только карточка со ссылкой

Первый инстинкт при загрузке аватарки — сунуть картинку в базу данных. Там же всё остальное лежит: имя, почта, пусть и фото полежит.

Так делать не надо, и вот почему. Картинка в базе превращается в текст в 1,3 раза длиннее оригинала. Аватарка на 2 МБ станет строкой на 2,7 МБ в каждой строке таблицы. Через сотню пользователей запрос «дай список юзеров» начнёт тащить сотни мегабайт, чтобы показать десять имён.

Правильная схема другая, и она же проще: файл едет на склад, в базу ложится только ссылка. Разберём по шагам.

Шаг 1. Заведи склад для файлов

Склад для картинок называется storage (или «бакет»). Это не база данных — это отдельное хранилище файлов, которое умеет отдавать их по ссылке.

Заводить свой сервер не нужно, у любого бэкенд-сервиса storage встроен. В Supabase это раздел Storage → New bucket. Назови его понятно, например avatars.

Один вопрос на этом шаге — публичный бакет или приватный:

  • Публичный — ссылку откроет кто угодно. Годится для аватарок, обложек, фото товаров.
  • Приватный — файл отдаётся по временной ссылке. Нужен для документов и всего, что не должно утечь.

Начни с публичного, если картинки и так все видят. Дальше в примерах — он.

Шаг 2. Поставь инпут в форму

Самая скучная часть, но у неё есть пара нюансов:

<input type="file" accept="image/*" id="avatar" />

accept="image/*" фильтрует диалог выбора файла — пользователь не увидит свои pdf. На телефоне этот же атрибут заодно предложит снять фото камерой.

Помни главное: accept — это подсказка, а не защита. Файл с любым содержимым всё равно можно подсунуть. Поэтому дальше шаг 3.

Шаг 3. Проверь файл до отправки

Две проверки, которые сэкономят тебе нервы:

const file = event.target.files[0]
if (!file) return

const MAX_MB = 5
if (file.size > MAX_MB * 1024 * 1024) {
  alert(`Файл больше ${MAX_MB} МБ — выбери поменьше`)
  return
}
if (!file.type.startsWith('image/')) {
  alert('Нужна картинка')
  return
}

Почему это важно: без лимита кто-нибудь зальёт видео на 200 МБ, ты оплатишь трафик, а страница будет грузиться минуту.

И честное предупреждение: проверка в браузере — удобство, а не безопасность. Она подсказывает нормальному человеку. Тот, кто хочет обойти, откроет консоль и обойдёт. Настоящий лимит ставится на стороне сервиса, в настройках бакета — там же есть и максимальный размер файла, и список разрешённых типов. Поставь их обязательно.

Шаг 4. Залей файл и получи ссылку

Сама загрузка — две строки:

const fileExt = file.name.split('.').pop()
const filePath = `${userId}-${Date.now()}.${fileExt}`

const { error } = await supabase.storage
  .from('avatars')
  .upload(filePath, file)

if (error) throw error

const { data } = supabase.storage
  .from('avatars')
  .getPublicUrl(filePath)

console.log(data.publicUrl)

Обрати внимание на filePath. Имя файла мы придумываем сами, а не берём из file.name. Причин три: пользовательские имена бывают с пробелами и кириллицей, два человека зальют разные photo.jpg и перезапишут друг друга, а имя файла с путём вроде ../../secret — это уже дырка в безопасности.

Ещё одна деталь: по умолчанию upload не перезаписывает существующий файл, а падает с ошибкой. Это защита от случайной затирки. Если тебе нужна именно замена (аватарку меняют), добавь { upsert: true }:

await supabase.storage.from('avatars').upload(filePath, file, { upsert: true })

Шаг 5. Сохрани ссылку в базе

Вот теперь база вступает в игру — и получает одну короткую строку:

await supabase
  .from('profiles')
  .update({ avatar_url: data.publicUrl })
  .eq('id', userId)

Всё. В таблице лежит текст на 80 символов вместо мегабайтов. Список пользователей снова летает, а картинки браузер тянет отдельно и параллельно — и кеширует, потому что отдаёт их CDN, а не твоя база данных.

Что получится

Рабочий цикл: человек выбрал файл → браузер отсёк слишком большой → файл уехал в бакет под уникальным именем → в профиле обновилась ссылка → аватарка видна с любого устройства, потому что живёт не на твоём ноуте.

Два бесплатных улучшения сверху, раз уж база устроена правильно.

Превью до загрузки — покажи выбранный файл сразу, не дожидаясь отправки:

const preview = URL.createObjectURL(file)
imgElement.src = preview

Уменьшение на лету — не отдавай аватарку 4000×3000 в кружочек 40 пикселей:

supabase.storage.from('avatars').getPublicUrl(filePath, {
  transform: { width: 200, height: 200 },
})

Одна строка — и вместо трёх мегабайт летит несколько килобайт.

Куда девать ключи от storage?

В браузере живёт только публичный ключ — он на то и публичный. Сервисный ключ, который обходит все правила доступа, в клиентский код класть нельзя никогда: он уедет в бандл и станет достоянием любого, кто откроет вкладку «Сеть». Подробнее — как безопасно хранить ключи.

Что делать со старой картинкой при замене?

Удалять — вручную, remove([oldPath]). Само ничего не удалится: залил три аватарки — на складе лежат три, платишь за три. Либо чисти старую после успешной замены, либо используй один и тот же путь с upsert: true — тогда файл просто перезапишется.

Учись вайб-кодингу, а не просто читай о нём

Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.

Открыть приложение
Робот KODiQ

ИИ-редактор KODiQ. Пишет про вайб-кодинг и AI-инструменты простым языком — каждый день.

Все статьи →