Работа с input type=“file” в JavaScript
Поле input type=“file” позволяет пользователю выбрать файл на своём устройстве. Через JavaScript можно получить сведения о выбранном файле, проверить его размер и тип, прочитать содержимое, показать предпросмотр или отправить файл на сервер.
Браузер не даёт странице свободный доступ к файловой системе. Код работает только с файлами, которые пользователь сам выбрал через поле или передал другим разрешённым способом.
- Разметка поля для выбора файла
- Свойство files и объект File
- Получение файла после выбора
- Выбор нескольких файлов
- Проверка типа и размера
- Чтение содержимого файла
- Предпросмотр изображения
- Отправка файла на сервер
- Очистка поля
- Итого
Разметка поля для выбора файла
Создадим форму для загрузки аватара:
<form
class="avatar-form"
action="/api/avatar"
method="post"
enctype="multipart/form-data"
>
<label for="avatar">Выберите изображение</label>
<input
id="avatar"
name="avatar"
type="file"
accept="image/jpeg,image/png,image/webp"
required
>
<p class="file-info" aria-live="polite">Файл не выбран</p>
<img
class="avatar-preview"
alt="Предпросмотр аватара"
hidden
>
<button type="submit">Загрузить</button>
<button class="clear-file" type="button">Убрать файл</button>
</form>
Атрибут name задаёт имя, под которым файл попадёт в данные формы. accept подсказывает браузеру, какие типы файлов показать в окне выбора, а required запрещает отправку формы без файла.
В accept можно перечислять MIME-типы или расширения через запятую. Например, значение .pdf,.docx подскажет браузеру предложить документы этих форматов.
Атрибут accept не проверяет файл и не гарантирует его безопасность. Пользователь может обойти ограничение, а сведения о типе файла могут быть неточными. Окончательная проверка всегда выполняется на сервере.
Свойство files и объект File
Выбранные файлы находятся в свойстве files:
const avatarForm = document.querySelector('.avatar-form');
const avatarInput = avatarForm.elements.avatar;
console.log(avatarInput.files); // FileList
files содержит объект FileList. Даже если разрешён выбор только одного файла, нужно обратиться к первому элементу коллекции:
const file = avatarInput.files[0];
if (file) {
console.log(file.name);
}
Каждый элемент коллекции является объектом File. В нём доступны основные сведения о файле:
| Свойство | Что содержит |
|---|---|
name | Имя файла без полного пути |
size | Размер в байтах |
type | MIME-тип, например image/png |
lastModified | Время последнего изменения в миллисекундах с 1 января 1970 года |
const file = avatarInput.files[0];
if (file) {
console.log(file.name);
console.log(file.size);
console.log(file.type);
console.log(new Date(file.lastModified));
}
Свойство value у поля тоже существует, но получать файл через него не нужно. В нём может находиться служебная строка наподобие C:\fakepath\avatar.png. Реальный путь к файлу браузер не раскрывает, поэтому работайте с input.files.
Получение файла после выбора
Событие change возникает, когда пользователь завершил выбор. В обработчике можно получить первый файл и вывести информацию о нём:
const fileInfo = document.querySelector('.file-info');
avatarInput.addEventListener('change', function () {
const file = avatarInput.files[0];
if (!file) {
fileInfo.textContent = 'Файл не выбран';
return;
}
const sizeInKilobytes = (file.size / 1024).toFixed(1);
fileInfo.textContent = `${file.name}, ${sizeInKilobytes} КБ`;
});
Проверка if (!file) не позволяет коду обратиться к свойствам отсутствующего объекта. Например, файла не будет, если поле уже очищено к моменту выполнения кода.
Выбор нескольких файлов
Чтобы разрешить выбор нескольких файлов, добавьте атрибут multiple:
<input name="photos" type="file" accept="image/*" multiple>
Все выбранные файлы окажутся в FileList. Коллекцию можно перебрать напрямую:
const photosInput = document.querySelector('input[name="photos"]');
photosInput.addEventListener('change', function () {
for (const file of photosInput.files) {
console.log(file.name, file.size);
}
});
FileList похож на массив, но не является обычным массивом. Если нужны методы map(), filter() или reduce(), сначала создайте массив:
const files = Array.from(photosInput.files);
const totalSize = files.reduce(function (sum, file) {
return sum + file.size;
}, 0);
console.log(totalSize);
Удалить отдельный элемент из FileList с помощью splice() нельзя. Эта коллекция доступна только для чтения. Обычно пользователь заново выбирает файлы, либо приложение очищает всё поле.
Проверка типа и размера
До чтения или отправки файла полезно проверить его на стороне браузера. Например, разрешим JPEG, PNG и WebP размером не больше 5 МБ:
const allowedFileTypes = new Set([
'image/jpeg',
'image/png',
'image/webp',
]);
const maxFileSize = 5 * 1024 * 1024;
function getFileError(file) {
if (!allowedFileTypes.has(file.type)) {
return 'Выберите изображение в формате JPEG, PNG или WebP';
}
if (file.size > maxFileSize) {
return 'Размер файла не должен превышать 5 МБ';
}
return '';
}
Используем эту функцию после выбора файла:
avatarInput.addEventListener('change', function () {
const file = avatarInput.files[0];
avatarInput.setCustomValidity('');
if (!file) {
fileInfo.textContent = 'Файл не выбран';
return;
}
const errorMessage = getFileError(file);
if (errorMessage) {
avatarInput.setCustomValidity(errorMessage);
fileInfo.textContent = errorMessage;
return;
}
const sizeInKilobytes = (file.size / 1024).toFixed(1);
fileInfo.textContent = `${file.name}, ${sizeInKilobytes} КБ`;
});
setCustomValidity() делает поле невалидным и сохраняет сообщение до тех пор, пока в метод не будет передана пустая строка. Именно поэтому ошибка очищается перед каждой новой проверкой.
Значение file.type иногда бывает пустым или неверным. Проверка расширения на стороне клиента тоже не даёт гарантии, потому что файл можно переименовать. Такие проверки помогают пользователю заметить ошибку раньше, но сервер всё равно должен самостоятельно проверить формат, размер и содержимое файла.
Чтение содержимого файла
Объект File содержит не только сведения о файле, но и его данные. Текстовый файл можно прочитать методом text():
<input class="text-file" type="file" accept=".txt,text/plain">
<pre class="file-content"></pre>
const textFileInput = document.querySelector('.text-file');
const fileContent = document.querySelector('.file-content');
textFileInput.addEventListener('change', async function () {
const file = textFileInput.files[0];
if (!file) {
fileContent.textContent = '';
return;
}
try {
const text = await file.text();
fileContent.textContent = text;
} catch (error) {
fileContent.textContent = 'Не удалось прочитать файл';
}
});
Метод text() возвращает промис со строкой. Для получения бинарных данных существует arrayBuffer(). В проектах также встречается FileReader - он решает похожую задачу с помощью событий, но для простого чтения текста метод file.text() обычно короче.
Оба способа чтения загружают содержимое в память страницы. Не читайте большой файл целиком без необходимости. Для отправки через FormData предварительно читать файл вообще не требуется.
Предпросмотр изображения
Для предпросмотра можно создать временный URL с помощью URL.createObjectURL():
const avatarPreview = document.querySelector('.avatar-preview');
let previewUrl = '';
function clearPreview() {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
previewUrl = '';
}
avatarPreview.removeAttribute('src');
avatarPreview.hidden = true;
}
avatarInput.addEventListener('change', function () {
clearPreview();
const file = avatarInput.files[0];
if (!file || !file.type.startsWith('image/')) {
return;
}
previewUrl = URL.createObjectURL(file);
avatarPreview.src = previewUrl;
avatarPreview.hidden = false;
});
avatarPreview.addEventListener('load', function () {
if (previewUrl) {
URL.revokeObjectURL(previewUrl);
previewUrl = '';
}
});
avatarForm.addEventListener('reset', clearPreview);
Временный URL действует только внутри текущей страницы. Когда он больше не нужен, его освобождают методом URL.revokeObjectURL(). После загрузки изображения браузер продолжит показывать его, даже если временный URL уже освобождён.
Отправка файла на сервер
Для отправки файла удобно использовать FormData. Конструктор получит именованные поля формы вместе с выбранным файлом:
avatarForm.addEventListener('submit', async function (event) {
event.preventDefault();
const file = avatarInput.files[0];
if (!file) {
fileInfo.textContent = 'Сначала выберите файл';
return;
}
const errorMessage = getFileError(file);
avatarInput.setCustomValidity(errorMessage);
if (errorMessage) {
avatarInput.reportValidity();
return;
}
const formData = new FormData(avatarForm);
try {
const response = await fetch(avatarForm.action, {
method: 'POST',
body: formData,
});
if (!response.ok) {
throw new Error('Сервер не принял файл');
}
avatarForm.reset();
fileInfo.textContent = 'Файл загружен';
} catch (error) {
fileInfo.textContent = 'Не удалось загрузить файл';
}
});
Не устанавливайте заголовок Content-Type вручную. При передаче FormData браузер сам сформирует multipart/form-data и добавит обязательную границу между частями запроса.
Адрес /api/avatar в примере условный. На сервере должен существовать обработчик, который принимает файл, повторно проверяет его и сохраняет в разрешённое место.
Очистка поля
Страница не может записать в файловое поле произвольный путь. Это ограничение защищает файлы пользователя. Через JavaScript полю разрешено присвоить пустую строку, чтобы убрать текущий выбор:
const clearFileButton = document.querySelector('.clear-file');
clearFileButton.addEventListener('click', function () {
avatarInput.value = '';
avatarInput.setCustomValidity('');
clearPreview();
fileInfo.textContent = 'Файл не выбран';
});
Метод avatarForm.reset() также очищает файловое поле и возвращает остальные элементы формы к исходным значениям.
Если поле не очищено, повторный выбор того же файла может не вызвать change, потому что значение не изменилось. Когда приложению нужно разрешить повторную обработку одного файла, очистите поле после завершения обработки.
Итого
Выбранные файлы доступны через input.files в виде коллекции FileList. Каждый объект File содержит имя, размер, MIME-тип, дату изменения и данные файла. Для нескольких файлов используется атрибут multiple.
До чтения и отправки проверяйте размер и ожидаемый тип, но не считайте проверку в браузере защитой сервера. Текст можно получить через file.text(), предпросмотр изображения - через временный URL, а для отправки подходит FormData. Очистить поле можно пустой строкой или сбросом всей формы.