Publicado: 27 de julio de 2020
Los navegadores pueden trabajar con archivos y directorios desde hace mucho tiempo. La API de File proporciona funciones para representar objetos de archivo en aplicaciones web, así como para seleccionarlos de forma programática y acceder a sus datos. Sin embargo, en cuanto te acercas, te das cuenta de que no todo lo que brilla es oro.
La forma tradicional de administrar archivos
Abrir archivos
Puedes abrir y leer archivos con el elemento <input type="file">.
En su forma más simple, abrir un archivo puede parecerse al ejemplo de código.
El objeto input te proporciona un FileList, que, en el caso de nuestro ejemplo, consta de un solo File.
Un File es un tipo específico de Blob y se puede usar en cualquier contexto en el que se pueda usar un 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();
});
};
Cómo abrir directorios
Para abrir carpetas (o directorios), puedes establecer el atributo <input webkitdirectory>.
Aparte de eso, todo lo demás funciona igual que antes.
A pesar de su nombre con prefijo de proveedor, webkitdirectory no solo se puede usar en navegadores Chromium y WebKit, sino también en el Edge heredado basado en EdgeHTML y en Firefox.
Cómo guardar y descargar archivos
Tradicionalmente, para guardar un archivo, solo puedes descargarlo, lo que funciona gracias al atributo <a download>.
Dado un Blob, puedes establecer el atributo href del elemento de anclaje en una URL de blob: que puedes obtener del método 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();
};
El problema
Una gran desventaja del enfoque de descarga es que no hay forma de realizar un flujo clásico de abrir→editar→guardar, es decir, no hay forma de reemplazar el archivo original. En cambio, cuando "guardas", obtienes una nueva copia del archivo original en la carpeta Descargas predeterminada del sistema operativo.
La API de File System Access
La API de File System Access simplifica mucho ambas operaciones, la de abrir y la de guardar. También permite el ahorro real. Esto significa que puedes elegir dónde guardar el archivo y reemplazar uno existente.
Abrir archivos
Con la API de File System Access, abrir un archivo es cuestión de una llamada al método window.showOpenFilePicker().
Esta llamada devuelve un identificador de archivo, desde el cual puedes obtener el File real a través del método 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);
}
};
Directorios abiertos
Abre un directorio llamando a window.showDirectoryPicker(), lo que hace que los directorios se puedan seleccionar en el cuadro de diálogo de archivo.
Cómo guardar archivos
Guardar archivos es igual de sencillo.
Desde un identificador de archivo, crea una transmisión grabable a través de createWritable(), luego escribe los datos del Blob llamando al método write() de la transmisión y, por último, cierra la transmisión llamando a su método 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);
}
};
Presentamos browser-fs-access
Si bien la API de File System Access es perfectamente adecuada, aún no está disponible de forma generalizada.
Por eso, considero que la API de File System Access es una mejora progresiva. Por lo tanto, quiero usarlo cuando el navegador lo admita y usar el enfoque tradicional si no lo hace, sin castigar nunca al usuario con descargas innecesarias de código JavaScript no compatible. La biblioteca browser-fs-access es mi respuesta a este desafío.
Filosofía de diseño
Dado que es probable que la API de File System Access cambie en el futuro, la API de browser-fs-access no se basa en ella.
Es decir, la biblioteca no es un polyfill, sino un ponyfill.
Puedes importar (de forma estática o dinámica) exclusivamente la funcionalidad que necesites para que tu app sea lo más pequeña posible.
Los métodos disponibles son fileOpen(), directoryOpen() y fileSave().
Internamente, la biblioteca detecta si se admite la API de File System Access y, luego, importa la ruta de código correspondiente.
Cómo usar la biblioteca
Los tres métodos son intuitivos de usar.
Puedes especificar el archivo mimeTypes o extensions aceptado de tu app, y establecer una marca multiple para permitir o rechazar la selección de varios archivos o directorios.
Para obtener todos los detalles, consulta la documentación de la API de browser-fs-access.
En la muestra de código, se muestra cómo puedes abrir y guardar archivos de imagen.
// 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',
});
})();
Demostración
Puedes ver el código en acción en una demostración de GitHub. Su código fuente también está disponible allí.
La biblioteca browser-fs-access en la naturaleza
En mi tiempo libre, contribuyo un poco a una PWA instalable llamada Excalidraw, una herramienta de pizarra que te permite dibujar diagramas con un aspecto dibujado a mano. Es completamente responsivo y funciona bien en una variedad de dispositivos, desde teléfonos celulares pequeños hasta computadoras con pantallas grandes. Esto significa que debe controlar archivos en todas las plataformas, independientemente de si admiten la API de File System Access. Esto la convierte en una excelente candidata para la biblioteca browser-fs-access.
Por ejemplo, puedo comenzar un dibujo en mi iPhone, guardarlo (técnicamente, descargarlo, ya que Safari no admite la API de File System Access) en la carpeta de descargas de mi iPhone, abrir el archivo en mi computadora de escritorio (después de transferirlo desde mi teléfono), modificarlo y sobrescribirlo con mis cambios, o incluso guardarlo como un archivo nuevo.
Muestra de código de la vida real
A continuación, puedes ver un ejemplo real de browser-fs-access tal como se usa en Excalidraw.
Este fragmento se extrajo de /src/data/json.ts.
Es de especial interés cómo el método saveAsJSON() pasa un identificador de archivo o null al método fileSave() de browser-fs-access, lo que hace que se reemplace cuando se proporciona un identificador o que se guarde en un archivo nuevo si no se proporciona.
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);
};
Consideraciones sobre la IU
Ya sea en Excalidraw o en tu app, la IU debe adaptarse a la situación de compatibilidad del navegador.
Si se admite la API de File System Access (if ('showOpenFilePicker' in window) {}), puedes mostrar un botón Guardar como además del botón Guardar.
Las capturas de pantalla a continuación muestran la diferencia entre la barra de herramientas principal responsiva de la app de Excalidraw en iPhone y en Chrome para computadoras.
Observa cómo falta el botón Guardar como en el iPhone.
Conclusiones
Técnicamente, trabajar con archivos del sistema funciona en todos los navegadores modernos. En los navegadores que admiten la API de File System Access, puedes mejorar la experiencia permitiendo que los archivos se guarden y se sobrescriban de verdad (no solo que se descarguen) y dejando que los usuarios creen archivos nuevos donde quieran, todo ello sin dejar de funcionar en los navegadores que no admiten la API de File System Access. browser-fs-access te facilita la vida, ya que se encarga de las sutilezas de la mejora progresiva y hace que tu código sea lo más simple posible.
Agradecimientos
Joe Medley y Kayce Basques revisaron este artículo. Gracias a los colaboradores de Excalidraw por su trabajo en el proyecto y por revisar mis solicitudes de extracción.