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 obsługi plików
Otwieranie plików
Jako deweloper możesz otwierać i odczytywać pliki za pomocą elementu
<input type="file">
.
W najprostszej postaci otwieranie pliku może wyglądać jak w przykładzie kodu poniżej.
Obiekt input
daje Ci FileList
, który w poniższym przypadku składa się tylko z jednego 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 (a raczej pobieranie) plików
W przypadku zapisywania pliku tradycyjnie możesz tylko pobrać plik, co działa 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 wykonać klasycznego procesu otwierania, edytowania i zapisywania, czyli nie można zastąpić oryginalnego pliku. Zamiast tego za każdym razem, gdy „zapisujesz” plik, w domyślnym folderze Pobrane pliki w systemie operacyjnym pojawia się nowa kopia oryginalnego pliku.
File System Access API
Interfejs File System Access API znacznie upraszcza obie operacje: otwieranie i zapisywanie. Umożliwia też prawdziwe zapisywanie, czyli możesz nie tylko wybrać miejsce, w którym chcesz zapisać plik, ale też zastąpić istniejący plik.
Otwieranie plików
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 pobrać 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.

Dlatego uważam File System Access API za 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 w przyszłości, 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órych potrzebujesz, aby aplikacja była jak najmniejsza.
Dostępne metody to fileOpen()
, directoryOpen()
i 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 browser-fs-access
Wszystkie 3 metody są intuicyjne.
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.
Poniższy 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
Powyższy kod możesz zobaczyć w wersji demonstracyjnej na GitHubie. Jego kod źródłowy jest również dostępny w tym miejscu. Ze względów bezpieczeństwa ramki podrzędne z innych domen nie mogą wyświetlać selektora plików, dlatego nie można umieścić wersji demonstracyjnej w tym artykule.
Biblioteka browser-fs-access w praktyce
W wolnym czasie współtworzę instalowalną aplikację PWA o nazwie Excalidraw. Jest to narzędzie do tablicy, które umożliwia łatwe tworzenie szkiców diagramów o wyglądzie rysunków odręcznych. 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 dobry 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.




Przykładowy kod z życia
Poniżej znajdziesz przykład użycia biblioteki 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 nadpisanie, gdy podany jest uchwyt, lub zapisanie w nowym pliku, jeśli nie.
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 Twojej 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 na komputerze z Chrome.
Zwróć uwagę, że na iPhonie brakuje przycisku 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 plików (nie tylko pobieranie) 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ę niuansami 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 za sprawdzenie moich próśb o scalenie. Baner powitalny autorstwa Ilyi Pavlova z Unsplash.