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.
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.
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.
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.