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

Przeglądarki już od dawna są w stanie radzić sobie z plikami i katalogami. 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. Jednak gdy przyjrzysz się bliżej, nie wszystko, co się świeci, nie jest złoto.

Tradycyjny sposób obsługi plików

Otwieranie plików

Jako programista możesz otwierać i odczytywać 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

Aby otworzyć foldery (lub katalogi), możesz ustawić atrybut <input webkitdirectory>. Poza tym reszta 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 plików tradycyjnie można tylko pobrać plik, co działa dzięki atrybucie <a download>. W przypadku Bloba 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” tworzysz 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 wybieranie katalogów w oknie pliku.

Zapisywanie plików

Zapisywanie plików jest podobne. Za pomocą uchwytu pliku tworzysz za pomocą createWritable() strumień z możliwością zapisu, następnie zapisujesz dane obiektu blob przez wywołanie metody write() strumienia, a 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 dostępnej w przeglądarce

Wszystkie 3 metody są intuicyjne w użyciu. Możesz określić akceptowany przez aplikację typ 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

Powyższy kod możesz zobaczyć w akcji w prezentacji na stronie Glitch. Jej kod źródłowy również jest dostępny w tym miejscu. Ze względów bezpieczeństwa nie można wyświetlać selektora plików w ramkach podrzędnych pochodzących z innych witryn, dlatego nie można umieścić w tym artykule wersji demonstracyjnej.

Biblioteka FS z dostępem do przeglądarki

W wolnym czasie pracuję w ramach instalacyjnej aplikacji PWA o nazwie Excalidraw – wirtualnej tablicy, która pozwala łatwo szkicować schematy za pomocą odręcznego rysunku. 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 różnych 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 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 czemu można uzyskać dostęp do pliku przez 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 niezmieniony.

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, w jaki sposób metoda saveAsJSON() przekazuje uchwyt pliku lub null do metody „browser-fs-access” fileSave(), co powoduje zastąpienie nicka po podaniu nicka lub – w przeciwnym razie – zapisanie go w nowym pliku.

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) {}), oprócz przycisku Zapisz możesz wyświetlać przycisk Zapisz jako. 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 za pomocą przycisku „Zapisz”.
Pasek narzędzi aplikacji Excalidraw app na iPhonie za pomocą przycisku Zapisz.
Pasek narzędzi aplikacji Excalidraw w Chrome na komputerze z przyciskami „Zapisz” i „Zapisz jako”.
Narzędzie do eksportowania aplikacji w Chrome z zaznaczonym przyciskiem Zapisz i zaznaczonym przyciskiem Zapisz 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. Interfejs browser-fs-access ułatwia codzienne funkcjonowanie, eliminując niuanse stopniowego ulepszania i maksymalnie upraszczając kod.

Podziękowania

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