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

Przeglądarki od dawna poradziły sobie z plikami i katalogami. Interfejs File API zapewnia funkcje umożliwiające prezentowanie obiektów plików w aplikacjach internetowych oraz automatyczne ich wybieranie i uzyskiwanie dostępu do ich danych. Jednak gdy spoglądasz bliżej, nie wszystko, co się świeci, nie jest złote.

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 przypominać przykładowy kod widoczny poniżej. Obiekt input udostępnia obiekt FileList, który w tym przypadku zawiera tylko 1 File. File to konkretny rodzaj elementu Blob, którego można używać w dowolnym kontekście dostępnym dla blobów.

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

Do otwierania folderów (lub katalogów) możesz ustawić atrybut <input webkitdirectory>. Poza tym wszystkie pozostałe funkcje działają tak samo jak powyżej. Pomimo nazwy, która jest prefiksem dostawcy, z webkitdirectory można korzystać nie tylko w przeglądarkach Chromium i WebKit, ale także w starszych, opartych na interfejsie EdgeHTML i Firefoksie.

Zapisywanie (a nie: pobieranie) plików

Aby zapisać plik, tradycyjnie wystarczy go pobrać, co jest możliwe dzięki atrybutowi <a download>. Dla obiektu blob możesz ustawić atrybut href kotwicy na adres URL blob:, który można uzyskać za pomocą 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

Główną wadą metody pobierania jest to, że nie można wykonać klasycznego procesu otwartego →edytuj →zapisz, tzn. nie ma możliwości zastępowania oryginalnego pliku. Zamiast tego przy każdym zapisie tworzona jest nowa kopia oryginalnego pliku w domyślnym folderze Pobrane pliki systemu operacyjnego.

Interfejs File System Access API

Interfejs File System Access API znacznie ułatwia wykonywanie operacji, a także otwieranie i zapisywanie. Umożliwia też prawdziwe zapisywanie, co oznacza, że możesz nie tylko wybrać miejsce zapisania pliku, ale także nadpisać istniejący plik.

Otwieranie plików

W przypadku interfejsu File System Access API otwarcie pliku wymaga jednego wywołania metody window.showOpenFilePicker(). To wywołanie zwraca uchwyt pliku, z którego można uzyskać faktyczny 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 metodę window.showDirectoryPicker(), która umożliwia wybór katalogów w oknie dialogowym pliku.

Zapisuję pliki

Zapisywanie plików wygląda podobnie. Na podstawie nicku pliku tworzysz strumień z możliwością zapisu przez createWritable(), następnie zapisujesz dane blobów, wywołując metodę 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);
  }
};

Przedstawiamy funkcję Browser-fs-access

Interfejs File System Access API jest w porządku, ale nie jest jeszcze powszechnie dostępny.

Tabela pomocy dotyczących przeglądarek na potrzeby interfejsu File System Access API. Wszystkie przeglądarki mają stan „Brak obsługi” lub „Za flagą”.
Tabela obsługiwanych przeglądarek dla interfejsu File System Access API. (Źródło)

Dlatego uważam interfejs File System Access API za progresywne ulepszenie. Dlatego chcę używać go, gdy przeglądarka go obsługuje, a w razie potrzeby – tradycyjnego podejścia – nigdy nie karać użytkowników niepotrzebnym pobieraniem nieobsługiwanego kodu JavaScript. Biblioteka browser-fs-access pomaga mi rozwiązać ten problem.

Filozofia projektowania

Interfejs File System Access API prawdopodobnie jeszcze się zmieni w przyszłości, więc interfejs API Browser-fs-access nie jest modelowany na jego podstawie. Oznacza to, że biblioteka to nie polyfill, a ponyfill; Możesz (statycznie lub dynamicznie) importować tylko te funkcje, które są niezbędne do utrzymania jak najmniejszego rozmiaru aplikacji. Dostępne metody to: 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 fs-access przeglądarki

Trzy metody są intuicyjne w użyciu. Możesz określić akceptowany przez aplikację mimeTypes lub plik extensions oraz ustawić flagę multiple, aby zezwolić na wybór wielu plików lub katalogów lub go zabronić. Więcej informacji znajdziesz w dokumentacji interfejsu Browser-fs-access API. Przykładowy kod poniżej pokazuje, jak otwierać i zapisywać pliki graficzne.

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

Pokaz

Powyższy kod możesz zobaczyć w prezentacji dotyczącej Glitch. Jej kod źródłowy również jest tam dostępny. Ze względów bezpieczeństwa podramki podrzędne z innych domen nie mogą wyświetlać selektora plików, więc nie można umieścić wersji demonstracyjnej w tym artykule.

Dostępna w przeglądarce biblioteka dostępu do plików FS

W wolnym czasie mogę pomóc w tworzeniu dostępnej do instalacji PWA o nazwie Excalidraw. Jest to narzędzie do wirtualnej tablicy umożliwiające łatwe szkicowanie diagramów rysowanych odręcznie. Jest w pełni responsywny 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 obsługiwać pliki na różnych platformach niezależnie od tego, czy obsługują interfejs File System Access API. Jest to więc świetna propozycja dla biblioteki dostępu do FS w przeglądarce.

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

Rysunek Excalidraw na iPhonie.
Rozpoczynam rysunek Excalidraw na iPhonie, gdy interfejs File System Access API nie jest obsługiwany, ale można go zapisać (pobrać) w folderze Pobrane pliki.
Zmodyfikowany rysunek Excalidraw w Chrome na pulpicie.
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 za pomocą interfejsu API.
Zastąpienie oryginalnego pliku zmianami.
Zastępowanie oryginalnego pliku zmianami w oryginalnym pliku rysunku Excalidraw. W przeglądarce pojawi się okno z pytaniem, czy wszystko jest w porządku.
Zapisywanie modyfikacji w nowym pliku rysunku Excalidraw.
Zapisywanie modyfikacji w nowym pliku Excalidraw. Oryginalny plik pozostanie niezmieniony.

Przykładowy kod w rzeczywistości

Poniżej możesz zobaczyć przykładowy kod przeglądarki fs-access używany w Excalidraw. Ten fragment pochodzi z: /src/data/json.ts. Szczególnie interesujące jest to, w jaki sposób metoda saveAsJSON() przekazuje nick pliku lub null do metody fileSave() fs-access, co powoduje zastępowanie jej po podaniu nicka lub zapisywanie w nowym pliku, jeśli nie jest.

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

Uwagi na temat interfejsu użytkownika

Interfejs powinien dostosowywać się do poziomu 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świetlić przycisk Zapisz jako. Na poniższych zrzutach ekranu widać różnicę między elastycznym głównym paskiem narzędzi aplikacji Excalidraw na iPhonie a Chrome na komputery. Zwróć uwagę, że na iPhonie brakuje przycisku Zapisz jako.

Pasek narzędzi aplikacji Excalidraw na iPhonie z przyciskiem „Zapisz”.
Pasek narzędzi aplikacji Excalidraw na iPhonie za pomocą przycisku Zapisz.
Pasek narzędzi aplikacji Excalidraw na komputerze Chrome z przyciskami „Zapisz” i „Zapisz jako”.
Usuń pasek narzędzi aplikacji w Chrome za pomocą przycisku Zapisz i zaznaczonego przycisku Zapisz jako.

Podsumowanie

Technicznie praca z plikami systemowymi działa we wszystkich nowoczesnych przeglądarkach. W przeglądarkach, które obsługują interfejs File System Access API, możesz polepszyć ich wrażenia, zezwalając na rzeczywiste zapisywanie i zastępowanie plików (a nie tylko na ich pobieranie) oraz umożliwiając użytkownikom tworzenie nowych plików w dowolnym miejscu i przy zachowaniu działania w przeglądarkach, które nie obsługują interfejsu File System Access API. Pole browser-fs-access ułatwia życie, eliminując subtelności stopniowego ulepszania i upraszczając tworzenie kodu.

Podziękowania

Ten artykuł napisali Joe Medley i Kayce Basques. Dziękujemy współtwórcom Excalidraw za pracę nad projektem i sprawdzanie moich żądań pull. Baner powitalny autorstwa Ilyi Pavlov w serwisie Unsplash.