Чтение и запись файлов и каталогов с помощью библиотеки браузера-fs-access.

Браузеры уже давно умеют работать с файлами и каталогами. 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 доступа к файловой системе. Все браузеры имеют пометку «нет поддержки» или «за флажком».
Таблица поддержки браузером 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, открыть файл на рабочем столе (предварительно перенеся его с моего телефон), измените файл и перезапишите его моими изменениями или даже сохраните как новый файл.

Рисунок Excalidraw на iPhone.
Запуск рисунка Excalidraw на iPhone, где API доступа к файловой системе не поддерживается, но где файл можно сохранить (загрузить) в папку «Загрузки».
Модифицированный рисунок Excalidraw в Chrome на рабочем столе.
Открытие и изменение чертежа Excalidraw на рабочем столе, где поддерживается API доступа к файловой системе, и, следовательно, доступ к файлу можно получить через API.
Перезапись исходного файла с изменениями.
Перезапись исходного файла с изменениями исходного файла чертежа Excalidraw. Браузер показывает диалоговое окно с вопросом, все ли в порядке.
Сохранение изменений в новом файле чертежа Excalidraw.
Сохранение изменений в новый файл Excalidraw. Исходный файл остается нетронутым.

Пример кода из реальной жизни

Ниже вы можете увидеть реальный пример браузерного доступа к 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 отсутствует кнопка «Сохранить как» .

Панель инструментов приложения Excalidraw на iPhone с единственной кнопкой «Сохранить».
Панель инструментов приложения Excalidraw на iPhone с помощью одной кнопки «Сохранить» .
Панель инструментов приложения Excalidraw на рабочем столе Chrome с кнопками «Сохранить» и «Сохранить как».
Панель инструментов приложения Excalidraw в Chrome с кнопкой «Сохранить» и выделенной кнопкой «Сохранить как» .

Выводы

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

Благодарности

Эта статья была рецензирована Джо Медли и Кейси Баскс . Спасибо участникам Excalidraw за их работу над проектом и за рассмотрение моих запросов на включение. Изображение героя Ильи Павлова на Unsplash.