Браузеры уже давно умеют работать с файлами и каталогами. File API предоставляет функции для представления файловых объектов в веб-приложениях, а также для их программного выбора и доступа к данным. Однако, если присмотреться, не всё то золото, что блестит.
Традиционный способ работы с файлами
Открытие файлов
Как разработчик, вы можете открывать и читать файлы через элемент <input type="file">
. В простейшем случае открытие файла может выглядеть примерно так, как показано в примере кода ниже. Объект input
возвращает вам FileList
, который в данном случае состоит только из одного File
. File
— это особый тип объекта Blob
, который может использоваться в любом контексте, в котором может использоваться объект Blob.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Открытие каталогов
Для открытия папок (или каталогов) можно установить атрибут <input webkitdirectory>
. В остальном всё работает так же, как описано выше. Несмотря на префикс вендора в названии, webkitdirectory
можно использовать не только в браузерах Chromium и WebKit, но и в устаревшем Edge на базе EdgeHTML, а также в Firefox.
Сохранение (скорее: загрузка) файлов
Традиционно для сохранения файла можно ограничиться его загрузкой , что реализовано благодаря атрибуту <a download>
. Для объекта Blob атрибут href
якоря можно установить на URL-адрес объекта blob:
который можно получить с помощью метода URL.createObjectURL()
.
const saveFile = async (blob) => {
const a = document.createElement('a');
a.download = 'my-file.txt';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', (e) => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
Проблема
Серьёзным недостатком подхода с загрузкой является невозможность реализовать классический процесс «открыть→редактировать→сохранить», то есть невозможность перезаписать исходный файл. Вместо этого при каждом «сохранении» в папке «Загрузки» операционной системы по умолчанию появляется новая копия исходного файла.
API доступа к файловой системе
API доступа к файловой системе значительно упрощает обе операции — открытие и сохранение. Он также обеспечивает «настоящее» сохранение , то есть возможность не только выбрать место сохранения файла, но и перезаписать существующий файл.
Открытие файлов
С помощью API доступа к файловой системе открытие файла осуществляется одним вызовом метода window.showOpenFilePicker()
. Этот вызов возвращает дескриптор файла, из которого можно получить сам File
с помощью метода getFile()
.
const openFile = async () => {
try {
// Always returns an array.
const [handle] = await window.showOpenFilePicker();
return handle.getFile();
} catch (err) {
console.error(err.name, err.message);
}
};
Открытие каталогов
Откройте каталог, вызвав метод window.showDirectoryPicker()
, который позволяет выбирать каталоги в диалоговом окне файла.
Сохранение файлов
Сохранение файлов выполняется так же просто. Из дескриптора файла вы создаёте доступный для записи поток с помощью метода createWritable()
, затем записываете данные BLOB-объекта, вызывая метод write()
потока, и, наконец, закрываете поток, вызывая его метод close()
.
const saveFile = async (blob) => {
try {
const handle = await window.showSaveFilePicker({
types: [{
accept: {
// Omitted
},
}],
});
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
} catch (err) {
console.error(err.name, err.message);
}
};
Представляем браузер-fs-access
Несмотря на все совершенство API доступа к файловой системе, он пока еще не получил широкого распространения .

Именно поэтому я рассматриваю API доступа к файловой системе как прогрессивное усовершенствование . Поэтому я хочу использовать его, когда браузер его поддерживает, и использовать традиционный подход, если нет; при этом не наказывая пользователя ненужной загрузкой неподдерживаемого JavaScript-кода. Библиотека browser-fs-access — мой ответ на этот вызов.
Философия дизайна
Поскольку API доступа к файловой системе, вероятно, изменится в будущем, API браузера (browser-fs-access) не создан по его образцу. То есть библиотека представляет собой не полифил , а скорее понифил . Вы можете (статически или динамически) импортировать исключительно любую необходимую функциональность, чтобы сохранить минимальную ёмкость приложения. Доступны методы с меткими названиями fileOpen()
, directoryOpen()
и fileSave()
. Библиотека определяет, поддерживается ли API доступа к файловой системе, а затем импортирует соответствующий путь к коду.
Использование библиотеки browser-fs-access
Все три метода интуитивно понятны в использовании. Вы можете указать поддерживаемые вашим приложением mimeTypes
или extensions
файлов, а также установить multiple
флаг, чтобы разрешить или запретить выбор нескольких файлов или каталогов. Подробности см. в документации по API браузера fs-access . В примере кода ниже показано, как открывать и сохранять файлы изображений.
// The imported methods will use the File
// System Access API or a fallback implementation.
import {
fileOpen,
directoryOpen,
fileSave,
} from 'https://unpkg.com/browser-fs-access';
(async () => {
// Open an image file.
const blob = await fileOpen({
mimeTypes: ['image/*'],
});
// Open multiple image files.
const blobs = await fileOpen({
mimeTypes: ['image/*'],
multiple: true,
});
// Open all files in a directory,
// recursively including subdirectories.
const blobsInDirectory = await directoryOpen({
recursive: true
});
// Save a file.
await fileSave(blob, {
fileName: 'Untitled.png',
});
})();
Демо
Вы можете увидеть этот код в действии в демо-версии на GitHub. Его исходный код также доступен там. Поскольку по соображениям безопасности вложенные фреймы с разными источниками не могут отображать окно выбора файлов, демо-версию невозможно встроить в эту статью.
Библиотека browser-fs-access в действии
В свободное время я немного работаю над устанавливаемым PWA под названием Excalidraw — инструментом для работы с доской, который позволяет легко рисовать схемы, словно нарисованные от руки. Он полностью адаптивен и отлично работает на различных устройствах, от небольших мобильных телефонов до компьютеров с большими экранами. Это означает, что он должен работать с файлами на всех платформах, независимо от того, поддерживают ли они API доступа к файловой системе. Это делает его отличным кандидатом для библиотеки browser-fs-access.
Например, я могу начать рисовать на своем iPhone, сохранить его (технически: загрузить, так как Safari не поддерживает API доступа к файловой системе) в папку «Загрузки» на моем iPhone, открыть файл на рабочем столе (после переноса его с телефона), изменить файл и перезаписать его своими изменениями или даже сохранить его как новый файл.




Пример кода из реальной жизни
Ниже представлен реальный пример использования браузера-fs-access в Excalidraw. Этот фрагмент взят из /src/data/json.ts
. Особый интерес представляет то, как метод saveAsJSON()
передаёт дескриптор файла или null
методу fileSave()
браузера-fs-access, что приводит к его перезаписыванию при получении дескриптора или сохранению в новом файле в противном случае.
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
fileHandle: any,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: "application/json",
});
const name = `${appState.name}.excalidraw`;
(window as any).handle = await fileSave(
blob,
{
fileName: name,
description: "Excalidraw file",
extensions: ["excalidraw"],
},
fileHandle || null,
);
};
export const loadFromJSON = async () => {
const blob = await fileOpen({
description: "Excalidraw files",
extensions: ["json", "excalidraw"],
mimeTypes: ["application/json"],
});
return loadFromBlob(blob);
};
Соображения относительно пользовательского интерфейса
Пользовательский интерфейс Excalidraw или вашего приложения должен адаптироваться к поддержке браузера. Если поддерживается API доступа к файловой системе ( if ('showOpenFilePicker' in window) {}
), можно отобразить кнопку «Сохранить как» в дополнение к обычной кнопке. На скриншотах ниже показана разница между адаптивной главной панелью инструментов Excalidraw на iPhone и в Chrome для ПК. Обратите внимание, что на iPhone кнопка « Сохранить как» отсутствует.


Выводы
Работа с системными файлами технически поддерживается во всех современных браузерах. В браузерах, поддерживающих API доступа к файловой системе, вы можете улучшить работу, разрешив полноценное сохранение и перезапись (а не только загрузку) файлов, а также позволив пользователям создавать новые файлы где угодно. При этом всё это останется функциональным и в браузерах, не поддерживающих API доступа к файловой системе. Модуль browser-fs-access упрощает вашу работу, позволяя учитывать тонкости прогрессивного улучшения и максимально упрощая код.
Благодарности
Эту статью рецензировали Джо Медли и Кейс Баскес . Выражаем благодарность участникам Excalidraw за их работу над проектом и рецензирование моих запросов на включение изменений. Изображение главного героя предоставлено Ильей Павловым на Unsplash.