Чтение и запись файлов и каталогов с помощью библиотеки браузера-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-access

Несмотря на все совершенство API доступа к файловой системе, он пока еще не получил широкого распространения .

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

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

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

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

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

Выводы

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

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

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