Браузеры уже давно умеют работать с файлами и каталогами. 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-доступ
Каким бы прекрасным ни был API доступа к файловой системе, он еще не широко доступен .
Вот почему я рассматриваю API доступа к файловой системе как прогрессивное усовершенствование . Таким образом, я хочу использовать его, когда браузер его поддерживает, и использовать традиционный подход, если нет; и при этом никогда не наказывать пользователя ненужной загрузкой неподдерживаемого кода JavaScript. Библиотека браузера-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, который используется в 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 доступа к файловой системе. не поддерживает API доступа к файловой системе. Browser-fs-access облегчает вашу жизнь, учитывая тонкости прогрессивного улучшения и делая ваш код максимально простым.
Благодарности
Эта статья была рецензирована Джо Медли и Кейси Баскс . Спасибо участникам Excalidraw за их работу над проектом и за рассмотрение моих запросов на включение. Изображение героя Ильи Павлова на Unsplash.