Odczytywanie i zapisywanie plików i katalogów

Opublikowano: 27 lipca 2020 r.

Przeglądarki od dawna radzą sobie z plikami i katalogami. File API udostępnia funkcje reprezentowania obiektów plików w aplikacjach internetowych, a także programowego wybierania ich i uzyskiwania dostępu do ich danych. Gdy jednak przyjrzysz się bliżej, zobaczysz, że nie wszystko złoto, co się świeci.

Tradycyjny sposób postępowania z plikami

Otwórz pliki

Pliki możesz otwierać i czytać za pomocą elementu <input type="file">. W najprostszej postaci otwieranie pliku może wyglądać jak w tym przykładzie kodu. Obiekt input zwraca FileList, który w naszym przykładzie składa się tylko z jednego elementu File. File to szczególny rodzaj Blob, którego można używać w każdym kontekście, w którym można używać obiektu Blob.

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 jest używany nie tylko w przeglądarkach Chromium i WebKit, ale także w starszej wersji Edge opartej na EdgeHTML oraz w Firefoksie.

Zapisywanie i pobieranie plików

Tradycyjnie, aby zapisać plik, możesz go tylko pobrać. Działa to dzięki atrybutowi <a download>. Mając obiekt Blob, możesz ustawić atrybut href elementu zakotwiczenia na adres URL blob:, który możesz 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

Ogromną wadą podejścia polegającego na pobieraniu jest to, że nie można zastosować klasycznego procesu otwierania, edytowania i zapisywania, czyli nie można zastąpić oryginalnego pliku. Zamiast tego za każdym razem, gdy „zapisujesz”, w domyślnym folderze Pobrane pliki w systemie operacyjnym pojawia się nowa kopia oryginalnego pliku.

File System Access API

File System Access API znacznie upraszcza obie operacje: otwieranie i zapisywanie. Umożliwia też prawdziwe oszczędzanie. Oznacza to, że możesz wybrać miejsce, w którym chcesz zapisać plik, i zastąpić istniejący plik.

Otwórz pliki

Dzięki File System Access API otwarcie pliku wymaga tylko jednego wywołania metody window.showOpenFilePicker(). To wywołanie zwraca uchwyt pliku, z którego możesz uzyskać rzeczywisty 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 dialogowym pliku.

Zapisywanie plików

Zapisywanie plików jest równie proste. Z uchwytu pliku tworzysz strumień do zapisu za pomocą metody createWritable(), następnie zapisujesz dane obiektu Blob, 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 browser-fs-access

Interfejs File System Access API jest bardzo dobry, ale nie jest jeszcze powszechnie dostępny.

Tabela obsługi interfejsu File System Access API w przeglądarkach. Wszystkie przeglądarki są oznaczone jako „nieobsługiwane” lub „za flagą”.
Tabela obsługi interfejsu File System Access API w przeglądarkach. (Źródło)

Dlatego uważam, że File System Access API to stopniowe ulepszenie. Dlatego chcę używać tej funkcji, gdy przeglądarka ją obsługuje, a w przeciwnym razie stosować tradycyjne podejście. Jednocześnie nie chcę obciążać użytkownika niepotrzebnym pobieraniem nieobsługiwanego kodu JavaScript. Biblioteka browser-fs-access jest moją odpowiedzią na to wyzwanie.

Filozofia projektowania

Interfejs File System Access API prawdopodobnie ulegnie jeszcze zmianom, dlatego interfejs browser-fs-access API nie jest na nim wzorowany. Oznacza to, że biblioteka nie jest polyfill, ale ponyfill. Możesz (statycznie lub dynamicznie) importować tylko te funkcje, które są Ci potrzebne, aby aplikacja była jak najmniejsza. Dostępne metody to fileOpen(), directoryOpen()fileSave(). Biblioteka wewnętrznie wykrywa, czy interfejs File System Access API jest obsługiwany, a następnie importuje odpowiednią ścieżkę kodu.

Korzystanie z biblioteki

Wszystkie 3 metody są intuicyjne w obsłudze. Możesz określić akceptowane mimeTypes lub pliki extensions aplikacji i ustawić flagę multiple, aby zezwolić na wybieranie wielu plików lub katalogów albo je zablokować. Szczegółowe informacje znajdziesz w dokumentacji interfejsu browser-fs-access API. Przykładowy kod 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

Kod możesz zobaczyć w akcji w wersji demonstracyjnej na GitHubie. Jego kod źródłowy jest również dostępny w tym miejscu.

Biblioteka browser-fs-access w praktyce

W wolnym czasie trochę pracuję nad instalowalną aplikacją PWA o nazwie Excalidraw, która jest narzędziem do tablicy, które umożliwia szkicowanie diagramów z odręcznym wyglądem. Jest w pełni responsywna 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ą one interfejs File System Access API. Dlatego jest to doskonały kandydat do biblioteki browser-fs-access.

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

Rysunek w Excalidraw na iPhonie.
Rozpoczęcie rysowania w Excalidraw na iPhonie, na którym interfejs File System Access API nie jest obsługiwany, ale plik można zapisać (pobrać) w folderze 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 za pomocą tego interfejsu.
zastąpienie oryginalnego pliku zmodyfikowaną wersją;
Zastąpienie oryginalnego pliku zmodyfikowanym plikiem rysunku Excalidraw. Przeglądarka wyświetla okno dialogowe z pytaniem, czy to w porządku.
Zapisywanie zmian w nowym pliku rysunku Excalidraw.
Zapisywanie zmian w nowym pliku Excalidraw. Oryginalny plik pozostaje niezmieniony.

Przykładowy kod z życia

Poniżej znajdziesz przykład użycia interfejsu browser-fs-access w aplikacji Excalidraw. Ten fragment pochodzi z /src/data/json.ts. Szczególnie interesujące jest to, jak metoda saveAsJSON() przekazuje uchwyt pliku lub null do metody browser-fs-access' fileSave(), co powoduje, że w przypadku podania uchwytu pliku następuje nadpisanie, a w przeciwnym razie zapisanie 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);
};

Wskazówki dotyczące interfejsu

Interfejs użytkownika w Excalidraw lub w aplikacji 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 responsywnym głównym paskiem narzędzi aplikacji Excalidraw na iPhonie i w Chrome na komputerze. 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 z samym przyciskiem Zapisz.
Pasek narzędzi aplikacji Excalidraw w Chrome z przyciskiem Zapisz i wybranym przyciskiem Zapisz jako.

Podsumowanie

Praca z plikami systemowymi jest technicznie możliwa we wszystkich nowoczesnych przeglądarkach. W przeglądarkach obsługujących interfejs File System Access API możesz ulepszyć działanie aplikacji, umożliwiając prawdziwe zapisywanie i nadpisywanie (a nie tylko pobieranie) plików oraz tworzenie nowych plików w dowolnym miejscu. Jednocześnie aplikacja będzie działać w przeglądarkach, które nie obsługują interfejsu File System Access API. Biblioteka browser-fs-access ułatwia życie, ponieważ zajmuje się subtelnościami progresywnego ulepszania i maksymalnie upraszcza kod.

Podziękowania

Ten artykuł został sprawdzony przez Joego Medleya i Kayce Basques. Dziękuję osobom, które przyczyniły się do rozwoju Excalidraw, za ich pracę nad projektem i sprawdzanie moich próśb o scalenie.