Odczytywanie i zapisywanie plików oraz katalogów za pomocą biblioteki fs-access w przeglądarce

Przeglądarki od dawna obsługują pliki i katalogi. Interfejs File API udostępnia funkcje umożliwiające reprezentowanie obiektów plików w aplikacjach internetowych, a także ich wybieranie i dostęp do ich danych za pomocą kodu. Gdy jednak przyjrzysz się bliżej, okaże się, że nie wszystko, co się świeci, to złoto.

Tradycyjny sposób obsługi plików

Otwieranie plików

Jako deweloper możesz otwierać i czytać pliki za pomocą elementu <input type="file">. W najprostszej formie otwarcie pliku może wyglądać jak w przykładowym kodzie poniżej. Obiekt input zawiera FileList, który w tym przypadku składa się tylko z jednego elementu File. File to konkretny rodzaj Blob, który może być używany w dowolnym kontekście, w którym można użyć Bloba.

const openFile = async () => {
  return new Promise((resolve) => {
    const input = document.createElement('input');
    input.type = 'file';
    input.addEventListener('change', () => {
      resolve(input.files[0]);
    });
    input.click();
  });
};

Otwieranie katalogów

W przypadku otwierania folderów (lub katalogów) możesz ustawić atrybut <input webkitdirectory>. Poza tym wszystko działa tak samo jak powyżej. Pomimo nazwy z prefiksem dostawcy webkitdirectory można używać nie tylko w przeglądarkach Chromium i WebKit, ale też w starszej przeglądarce Edge opartej na EdgeHTML oraz w Firefoxie.

Zapisywanie (czyli pobieranie) plików

W przypadku zapisywania pliku tradycyjnie ogranicza się to do pobierania pliku, co działa dzięki atrybucie <a download>. W przypadku pliku Blob możesz ustawić atrybut href kotwicy na adres URL blob:, który możesz uzyskać z metody 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();
};

Problem

Dużym minusem podejścia polegającego na pobieraniu jest to, że nie można użyć klasycznego procesu otwierania, edytowania i zapisywania, czyli nie można zastąpić oryginalnego pliku. Zamiast tego po każdym zapisaniu pliku otrzymujesz nową kopię oryginalnego pliku w domyślnym folderze Pobrane w systemie operacyjnym.

Interfejs File System Access API

Interfejs File System Access API znacznie upraszcza otwieranie i zapisywanie plików. Umożliwia też prawdziwe zapisywanie, czyli nie tylko wybranie miejsca zapisu pliku, ale też zastąpienie istniejącego pliku.

Otwieranie plików

Dzięki interfejsowi File System Access API otwarcie pliku polega na wywołaniu metody window.showOpenFilePicker(). To wywołanie zwraca uchwyt pliku, z którego możesz uzyskać rzeczywiste dane File za pomocą metody 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);
  }
};

Otwieranie katalogów

Otwórz katalog, wywołując funkcję window.showDirectoryPicker(), która umożliwia wybór katalogów w oknie dialogowym pliku.

Zapisywanie plików

Zapisywanie plików jest równie proste. Na podstawie uchwytu pliku tworzysz strumień do zapisu za pomocą funkcji createWritable(), a następnie zapisujesz dane Blob, wywołując metodę write() strumienia. Na koniec zamykasz strumień, wywołując jego metodę 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);
  }
};

Wprowadzenie funkcji browser-fs-access

Mimo że interfejs File System Access API jest świetny, nie jest jeszcze powszechnie dostępny.

Tabela obsługi przeglądarek w przypadku interfejsu File System Access API. Wszystkie przeglądarki są oznaczone jako „brak obsługi” lub „zablokowane”.
Tabela obsługi przeglądarek w przypadku interfejsu File System Access API. (źródło)

Dlatego uważam, że interfejs File System Access API jest ulepszeniem stopniowym. Dlatego chcę używać go, gdy przeglądarka go obsługuje, a w przeciwnym razie używać tradycyjnego podejścia. Nie chcę też karać użytkownika niepotrzebnym pobieraniem nieobsługiwanego kodu JavaScript. Biblioteka browser-fs-access jest odpowiedzią na to wyzwanie.

Filozofia projektowania

Ponieważ interfejs File System Access API może jeszcze ulec zmianie, interfejs browser-fs-access API nie jest na nim wzorowany. Oznacza to, że biblioteka nie jest polyfillem, ale ponyfillem. Możesz (statycznie lub dynamicznie) importować tylko te funkcje, których potrzebujesz, aby aplikacja była jak najmniejsza. Dostępne metody to odpowiednio fileOpen(), directoryOpen() i fileSave(). Wewnętrznie funkcja biblioteki wykrywa, czy interfejs File System Access API jest obsługiwany, a następnie importuje odpowiedni ścieżkę kodu.

Korzystanie z biblioteki browser-fs-access

Wszystkie 3 metody są intuicyjne w użyciu. Możesz określić akceptowane przez aplikację mimeTypes lub plik extensions, a także ustawić flagę multiple, aby zezwolić lub zabronić wyboru wielu plików lub katalogów. Pełne informacje znajdziesz w dokumentacji interfejsu API browser-fs-access. Przykładowy kod poniżej pokazuje, jak otwierać i zapisywać pliki obrazów.

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

Prezentacja

Ten kod możesz zobaczyć w akcji w prezentacji na Glitch. Tam też znajdziesz kod źródłowy. Ze względów bezpieczeństwa podrzędne ramki między domenami nie mogą wyświetlać selektora plików, dlatego nie można osadzić w tym artykule wersji demonstracyjnej.

Biblioteka browser-fs-access w praktyce

W wolnym czasie pracuję nad instalowaną PWA o nazwie Excalidraw. To narzędzie do tablicy, które pozwala łatwo szkicować diagramy w sposób przypominający rysowanie odręczne. Jest on w pełni elastyczny i działa dobrze na różnych urządzeniach, od małych telefonów komórkowych po komputery z dużymi ekranami. Oznacza to, że musi on obsługiwać pliki na wszystkich platformach, niezależnie od tego, czy obsługują one interfejs File System Access API. Dzięki temu jest to świetny kandydat do biblioteki browser-fs-access.

Mogę na przykład zacząć rysować na iPhonie, zapisać rysunek (technicznie: pobrać, ponieważ Safari nie obsługuje interfejsu File System Access API) do folderu Pobrane na iPhonie, otworzyć plik na pulpicie (po przeniesieniu go z telefonu), zmodyfikować go i zastąpić wprowadzonymi zmianami lub nawet zapisać jako nowy plik.

Rysunek w Excalidraw na iPhonie.
Rozpoczynanie tworzenia rysunku w Excalidraw na iPhonie, gdzie interfejs API File System Access nie jest obsługiwany, ale plik można zapisać (pobrać) do folderu Pobrane.
Zmodyfikowany rysunek w programie Excalidraw w Chrome na komputerze.
Otwieranie i modyfikowanie rysunku Excalidraw na komputerze, na którym obsługiwany jest interfejs File System Access API, dzięki któremu można uzyskać dostęp do pliku za pomocą interfejsu API.
Zastępowanie oryginalnego pliku zmodyfikowanym plikiem.
Zastępowanie oryginalnego pliku zmodyfikowanym plikiem rysunku Excalidraw. Przeglądarka wyświetla okno z pytaniem, czy na pewno chcę to zrobić.
Zapisz zmiany w nowym pliku rysunku Excalidraw.
Zapisywanie zmian w nowym pliku Excalidraw. Oryginalny plik pozostaje nienaruszony.

Przykładowy kod z życia

Poniżej możesz zobaczyć przykład użycia w Excalidraw uprawnienia browser-fs-access. Ten fragment pochodzi z /src/data/json.ts. Szczególnie interesujące jest to, jak metoda saveAsJSON() przekazuje do metody browser-fs-access handle pliku lub wartość null, co powoduje zastąpienie wartości, jeśli podano handle, lub zapisanie do nowego pliku, jeśli nie.fileSave()

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

Interfejs użytkownika

W Excalidraw lub Twojej aplikacji interfejs powinien dostosowywać się do obsługi przeglądarki. Jeśli interfejs File System Access API jest obsługiwany (if ('showOpenFilePicker' in window) {}), możesz wyświetlić przycisk Zapisz jako oprócz przycisku Zapisz. Zrzuty ekranu poniżej pokazują różnicę między elastycznym paskiem narzędzi aplikacji Excalidraw na iPhone'a a Chrome na komputerze. Na iPhonie nie ma przycisku Zapisz jako.

Pasek narzędzi aplikacji Excalidraw na iPhonie z jednym przyciskiem „Zapisz”.
Pasek narzędzi aplikacji Excalidraw na iPhonie z jednym przyciskiem Zapisz.
Pasek narzędzi aplikacji Excalidraw w Chrome na komputerze z przyciskami „Zapisz” i „Zapisz jako”.
Pasek narzędzi aplikacji Excalidraw w Chrome z przyciskami ZapiszZapisz jako.

Podsumowanie

Praca z plikami systemowymi jest teoretycznie możliwa we wszystkich nowoczesnych przeglądarkach. W przeglądarkach, które obsługują interfejs File System API, możesz ulepszyć działanie aplikacji, zezwalając na prawdziwe zapisywanie i nadpisywanie (a nie tylko pobieranie) plików oraz pozwalając użytkownikom na tworzenie nowych plików w dowolnym miejscu. Wszystko to przy zachowaniu funkcjonalności w przeglądarkach, które nie obsługują interfejsu File System API. browser-fs-access ułatwia życie, ponieważ dba o szczegóły związane z ulepszaniem stopniowym i utrzymuje kod w jak największej prostocie.

Podziękowania

Ten artykuł został sprawdzony przez Joe Medley i Kayce Basques. Dziękujemy wszystkim współtwórcom Excalidraw za pracę nad projektem i sprawdzenie moich Pull Request. Baner powitalny autorstwa Ilya Pavlov z Unsplash.