File System Access API

Lit — это лёгкий и производительный фреймворк для создания веб-компонентов на основе стандартов Web Components. Он предоставляет удобные инструменты для реактивного обновления интерфейса, шаблонов и управления состоянием компонентов. Одной из интересных возможностей современного веба является работа с файловой системой через File System Access API, которое позволяет веб-приложениям читать и записывать файлы на локальном устройстве пользователя с его явного разрешения.

Подключение Lit и базовый компонент

Для начала работы с Lit необходимо установить пакет:

import { LitElement, html, css } from 'lit';

Создание простого компонента:

class FileEditor extends LitElement {
  static properties = {
    fileContent: { type: String }
  };

  constructor() {
    super();
    this.fileContent = '';
  }

  render() {
    return html`
      <textarea .value=${this.fileContent} @input=${this.updateContent}></textarea>
      <button @click=${this.openFile}>Открыть файл</button>
      <button @click=${this.saveFile}>Сохранить файл</button>
    `;
  }

  updateContent(event) {
    this.fileContent = event.target.value;
  }
}

customElements.define('file-editor', FileEditor);

Работа с File System Access API

File System Access API предоставляет два основных интерфейса: FileSystemFileHandle для работы с отдельными файлами и FileSystemDirectoryHandle для работы с папками. Доступ к ним осуществляется через вызовы window.showOpenFilePicker() или window.showSaveFilePicker().

Открытие файла

async openFile() {
  try {
    const [fileHandle] = await window.showOpenFilePicker({
      types: [
        {
          description: 'Текстовые файлы',
          accept: { 'text/plain': ['.txt'] }
        }
      ]
    });
    const file = await fileHandle.getFile();
    this.fileContent = await file.text();
  } catch (err) {
    console.error('Ошибка при открытии файла:', err);
  }
}

Ключевые моменты:

  • showOpenFilePicker возвращает массив, даже если выбран только один файл.
  • fileHandle.getFile() возвращает объект File, из которого можно прочитать содержимое через text() или arrayBuffer().

Сохранение файла

async saveFile() {
  try {
    const fileHandle = await window.showSaveFilePicker({
      suggestedName: 'document.txt',
      types: [
        {
          description: 'Текстовые файлы',
          accept: { 'text/plain': ['.txt'] }
        }
      ]
    });

    const writable = await fileHandle.createWritable();
    await writable.write(this.fileContent);
    await writable.close();
  } catch (err) {
    console.error('Ошибка при сохранении файла:', err);
  }
}

Особенности:

  • createWritable() возвращает объект для записи данных.
  • Запись должна завершаться методом close() для фиксации изменений.
  • showSaveFilePicker позволяет указать рекомендуемое имя файла и типы файлов.

Интеграция реактивности Lit с файловой системой

Lit автоматически обновляет DOM при изменении реактивных свойств, таких как fileContent. Это позволяет создавать полноценные редакторы, где изменение текста в textarea синхронно отображается в состоянии компонента и может быть сохранено на диск.

Пример расширения функциональности:

class FileEditor extends LitElement {
  static properties = {
    fileContent: { type: String },
    fileName: { type: String }
  };

  constructor() {
    super();
    this.fileContent = '';
    this.fileName = '';
  }

  render() {
    return html`
      <h3>${this.fileName || 'Новый файл'}</h3>
      <textarea .value=${this.fileContent} @input=${this.updateContent}></textarea>
      <div>
        <button @click=${this.openFile}>Открыть</button>
        <button @click=${this.saveFile}>Сохранить</button>
      </div>
    `;
  }

  updateContent(event) {
    this.fileContent = event.target.value;
  }

  async openFile() {
    try {
      const [fileHandle] = await window.showOpenFilePicker();
      const file = await fileHandle.getFile();
      this.fileContent = await file.text();
      this.fileName = file.name;
    } catch (err) {
      console.error(err);
    }
  }

  async saveFile() {
    try {
      const fileHandle = await window.showSaveFilePicker({
        suggestedName: this.fileName || 'document.txt'
      });
      const writable = await fileHandle.createWritable();
      await writable.write(this.fileContent);
      await writable.close();
      this.fileName = fileHandle.name;
    } catch (err) {
      console.error(err);
    }
  }
}

customElements.define('file-editor', FileEditor);

Расширенные возможности

  • Поддержка нескольких файлов: showOpenFilePicker({ multiple: true }) возвращает массив файловых дескрипторов.
  • Работа с каталогами: window.showDirectoryPicker() позволяет получить доступ к папкам и их содержимому, читать и создавать файлы внутри директории.
  • Управление правами: API предоставляет возможность проверять и запрашивать разрешения через queryPermission() и requestPermission().

Важные аспекты безопасности

  • Доступ к файловой системе возможен только через явное действие пользователя (клик на кнопку).
  • Веб-приложения не могут произвольно читать или записывать файлы без разрешения.
  • Ограничения браузеров: на данный момент API поддерживается в Chromium-браузерах и требует HTTPS.

Оптимизация работы с файлами

Для больших файлов рекомендуется использовать потоковую запись через writable.write() с Blob частями, чтобы избежать блокировки интерфейса.

const chunk = new Blob([this.fileContent.slice(0, 1024)]);
await writable.write(chunk);

Lit обеспечивает плавное обновление интерфейса, что позволяет безопасно использовать File System Access API даже при работе с большими текстами или бинарными данными.