Os navegadores lidam com arquivos e diretórios há muito tempo. A API File oferece recursos para representar objetos de arquivo em aplicativos da Web, além de selecioná-los e acessar os dados de maneira programática. No entanto, quando você olha mais de perto, nem tudo que reluz é ouro.
A forma tradicional de lidar com arquivos
Abrir arquivos
Como desenvolvedor, você pode abrir e ler arquivos usando o
elemento
<input type="file">
.
Em sua forma mais simples, abrir um arquivo pode ser semelhante ao exemplo de código abaixo.
O objeto input
fornece um FileList
,
que, no caso abaixo, consiste em apenas um
File
.
Um File
é um tipo específico de Blob
e pode ser usado em qualquer contexto que um Blob possa.
const openFile = async () => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
input.addEventListener('change', () => {
resolve(input.files[0]);
});
input.click();
});
};
Como abrir diretórios
Para abrir pastas (ou diretórios), defina o
atributo
<input webkitdirectory>
.
Além disso, todo o restante funciona da mesma forma acima.
Apesar do nome prefixado pelo fornecedor,
o webkitdirectory
não é apenas utilizável nos navegadores Chromium e WebKit, mas também no Edge legado baseado em EdgeHTML, bem como no Firefox.
Salvar (em vez de fazer o download) de arquivos
Tradicionalmente, para salvar um arquivo, você só pode fazer o download de um arquivo,
o que funciona graças ao
atributo
<a download>
.
Dado um Blob, você pode definir o atributo href
da âncora como um URL blob:
que pode ser recebido do
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();
};
O problema
Uma grande desvantagem da abordagem de download é que não há como fazer um fluxo clássico de abrir → editar → salvar acontecer, ou seja, não há como substituir o arquivo original. Em vez disso, você vai acabar com uma nova cópia do arquivo original na pasta "Downloads" padrão do sistema operacional sempre que "salvar".
A API File System Access
A API File System Access simplifica muito as operações, como abrir e salvar. Ela também permite o salvamento real, ou seja, você não só pode escolher onde salvar um arquivo, mas também substituir um arquivo existente.
Abrir arquivos
Com a API File System Access,
para abrir um arquivo, basta uma chamada para o método window.showOpenFilePicker()
.
Essa chamada retorna um identificador de arquivo, de onde você pode acessar o File
real usando o 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);
}
};
Como abrir diretórios
Abra um diretório chamando
window.showDirectoryPicker()
, que torna os diretórios selecionáveis na caixa de diálogo do arquivo.
Como salvar arquivos
Salvar arquivos é igualmente simples.
Em um gerenciador de arquivo, crie um stream gravável por createWritable()
e grave os dados do Blob chamando o método write()
do stream
e, por fim, feche o stream chamando o 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);
}
};
Apresentando browser-fs-access
A API File System Access pode ser perfeita, mas ainda não está amplamente disponível.
É por isso que vejo a API File System Access como um aprimoramento progressivo. Sendo assim, quero usá-lo quando o navegador oferecer suporte a ele e, caso contrário, usar a abordagem tradicional, sem punir o usuário com downloads desnecessários de código JavaScript sem suporte. A biblioteca browser-fs-access é a minha resposta para esse desafio.
Filosofia de design
Como a API File System Access ainda provavelmente será alterada no futuro,
a API browser-fs-access não é modelada com base nela.
Ou seja, a biblioteca não é um polyfill,
mas um preenchimento de pônei.
Você pode importar (estática ou dinamicamente) exclusivamente qualquer funcionalidade necessária para manter o app o menor possível.
Os métodos disponíveis são os nomes
fileOpen()
,
directoryOpen()
e
fileSave()
.
Internamente, o recurso da biblioteca detecta se há suporte para a API File System Access
e importa o caminho do código correspondente.
Como usar a biblioteca browser-fs-access
Os três métodos são intuitivos.
Você pode especificar a mimeTypes
aceita pelo app ou o extensions
do arquivo e definir uma sinalização multiple
para permitir ou proibir a seleção de vários arquivos ou diretórios.
Para mais detalhes, consulte a documentação da API browser-fs-access.
O exemplo de código abaixo mostra como abrir e salvar arquivos de imagem.
// 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',
});
})();
Demonstração
Veja o código acima em ação em uma demonstração do Glitch. O código-fonte também está disponível nessa seção. Como os subframes de origem cruzada não podem mostrar um seletor de arquivos por motivos de segurança, a demonstração não pode ser incorporada a este artigo.
A biblioteca browser-fs-access em execução
No meu tempo livre, contribuo um pouco para um PWA instalável chamado Excalidraw, uma ferramenta de lousa interativa que permite esboçar diagramas facilmente com uma sensação de desenho à mão. Ele é totalmente responsivo e funciona bem em vários dispositivos, desde smartphones pequenos até computadores com telas grandes. Isso significa que ele precisa processar arquivos em todas as plataformas, independentemente de serem compatíveis ou não com a API File System Access. Isso o torna um ótimo candidato para a biblioteca browser-fs-access.
Posso, por exemplo, iniciar um desenho no meu iPhone, salvá-lo (tecnicamente: fazer o download, já que o Safari não é compatível com a API File System Access) na minha pasta de downloads do iPhone, abrir o arquivo na área de trabalho (depois de transferi-lo do meu smartphone), modificar o arquivo e substituí-lo pelas minhas alterações ou até mesmo salvá-lo como um novo arquivo.
Exemplo de código real
Abaixo, você pode conferir um exemplo real de browser-fs-access como é usado no Excalidraw.
Este trecho foi retirado de
/src/data/json.ts
.
É interessante como o método saveAsJSON()
transmite um identificador de arquivo ou null
para o método fileSave()
do navegador-fs-access, o que faz com que ele seja substituído quando um identificador é fornecido ou, caso contrário, para salvar em um novo arquivo.
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);
};
Considerações sobre a interface
Seja no Excalidraw ou no seu app,
a interface precisa se adaptar à situação de suporte do navegador.
Se houver suporte para a API File System Access (if ('showOpenFilePicker' in window) {}
),
você poderá mostrar um botão Save As e Save.
As capturas de tela abaixo mostram a diferença entre a barra de ferramentas principal responsiva do Excalidraw no iPhone e no Chrome para computador.
Observe como o botão Save As está ausente no iPhone.
Conclusões
Tecnicamente, trabalhar com arquivos de sistema funciona em todos os navegadores mais recentes. Em navegadores compatíveis com a API File System Access, você pode melhorar a experiência permitindo salvar e substituir (não apenas fazer o download) de arquivos e permitir que os usuários criem novos arquivos onde quiserem, tudo isso enquanto continua funcional em navegadores que não são compatíveis com a API File System Access. O browser-fs-access facilita sua vida ao lidar com as sutilezas do aprimoramento progressivo e tornar seu código o mais simples possível.
Agradecimentos
Este artigo foi revisado por Joe Medley e Kayce Basques. Agradeço aos colaboradores do Excalidraw pelo trabalho no projeto e por analisarem minhas solicitações de envio. Imagem principal por Ilya Pavlov (link em inglês) no Unsplash.