Браузеры уже давно умеют работать с файлами и каталогами. 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
якоря на blob:
URL, который вы можете получить из метода 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 File System Access значительно упрощает обе операции, открытие и сохранение. Он также позволяет осуществлять настоящее сохранение , то есть вы можете не только выбрать, где сохранить файл, но и перезаписать существующий файл.
Открытие файлов
С помощью File System Access 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 File System Access как прогрессивное улучшение . Таким образом, я хочу использовать его, когда браузер его поддерживает, и использовать традиционный подход, если нет; и при этом никогда не наказывать пользователя ненужными загрузками неподдерживаемого кода JavaScript. Библиотека browser-fs-access — мой ответ на этот вызов.
Философия дизайна
Поскольку API доступа к файловой системе, скорее всего, изменится в будущем, API браузера fs-access не смоделирован по его образцу. То есть библиотека не является полифиллом , а скорее понифиллом . Вы можете (статически или динамически) импортировать исключительно любую необходимую вам функциональность, чтобы ваше приложение оставалось как можно меньше. Доступные методы имеют меткие названия fileOpen()
, directoryOpen()
и fileSave()
. Внутри библиотека определяет, поддерживается ли API доступа к файловой системе, а затем импортирует соответствующий путь кода.
Использование библиотеки браузера-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',
});
})();
Демо
Вы можете увидеть приведенный выше код в действии в демо на Glitch. Его исходный код также доступен там. Поскольку по соображениям безопасности кросс-источниковые подфреймы не могут отображать средство выбора файлов, демо не может быть встроено в эту статью.
Библиотека браузера-fs-access в действии
В свободное время я немного помогаю устанавливаемому PWA под названием Excalidraw , инструменту для доски, который позволяет вам легко рисовать диаграммы с ощущением, что вы нарисованы от руки. Он полностью адаптивен и хорошо работает на различных устройствах от небольших мобильных телефонов до компьютеров с большими экранами. Это означает, что он должен иметь дело с файлами на всех различных платформах, независимо от того, поддерживают ли они API доступа к файловой системе. Это делает его отличным кандидатом для библиотеки браузера 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 за их работу над проектом и за рецензирование моих Pull Requests. Изображение героя от Ильи Павлова на Unsplash.