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

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

Рисунок 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 за их работу над проектом и за рецензирование моих Pull Requests. Изображение героя от Ильи Павлова на Unsplash.