Чтение и запись файлов и каталогов.

Опубликовано: 27 июля 2020 г.

Браузеры уже давно умеют работать с файлами и каталогами. File API предоставляет возможности для представления файловых объектов в веб-приложениях, а также для программного выбора файлов и доступа к их данным. Однако, если присмотреться, оказывается, что не всё то золото, что блестит.

Традиционный способ работы с файлами

Открыть файлы

You can open and read files with the <input type="file"> element. In its simplest form, opening a file can look something like the code sample. The input object gives you a FileList , which in the case of our example, consists of just one File . A File is a specific kind of Blob , and can be used in any context that a Blob can.

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();
};

Проблема

A massive downside of the download approach is that there is no way to make a classic open→edit→save flow happen, that is, there is no way to overwrite the original file. Instead, you end up with a new copy of the original file in the operating system's default Downloads folder whenever you "save".

API доступа к файловой системе

The File System Access API makes both operations, opening and saving, a lot simpler. It also enables true saving . This means you can choose where to save the file and to overwrite an existing file.

Открыть файлы

With the File System Access API , opening a file is a matter of one call to the window.showOpenFilePicker() method. This call returns a file handle, from which you can get the actual File via the getFile() method.

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);
  }
};

Представляем browser-fs-access

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

Browser support table for the File System Access API. All browsers are marked as 'no support' or 'behind a flag'.
Таблица поддержки браузерами API доступа к файловой системе. ( Источник )

This is why I see the File System Access API as a progressive enhancement . As such, I want to use it when the browser supports it, and use the traditional approach if not; all while never punishing the user with unnecessary downloads of unsupported JavaScript code. The browser-fs-access library is my answer to this challenge.

философия дизайна

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

Воспользуйтесь библиотекой

The three methods are intuitive to use. You can specify your app's accepted mimeTypes or file extensions , and set a multiple flag to allow or disallow selection of multiple files or directories. For full details, see the browser-fs-access API documentation . The code sample shows how you can open and save image files.

// 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.
Saving the modifications to a new Excalidraw file. The original file remains untouched.

Реальный пример кода

Below, you can see an actual example of browser-fs-access as it is used in Excalidraw. This excerpt is taken from /src/data/json.ts . Of special interest is how the saveAsJSON() method passes either a file handle or null to browser-fs-access' fileSave() method, which causes it to overwrite when a handle is given, or to save to a new file if not.

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 с кнопкой «Сохранить» и активной кнопкой «Сохранить как» .

Выводы

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

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

Этот код был проверен Джо Медли и Кейси Баскес . Спасибо участникам проекта Excalidraw за их работу и за проверку моих запросов на слияние (Pull Requests).