Как добавить загрузку картинок в приложение — 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 — тогда файл просто перезапишется.
Короткие уроки-истории, симулятор агента и ежедневная практика — в нашем мобильном приложении. Бесплатно.





