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.
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.
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.
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 Medley i Kayce 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.