Qualsiasi tecnologia sufficientemente avanzata è indistinguibile dalla magia. A meno che non lo capisci. Mi chiamo Thomas Steiner, lavoro nel team Developer Relations di Google e in questo articolo del mio talk al Google I/O esaminerò alcune delle nuove API Fugu e il modo in cui migliorano i percorsi degli utenti principali nella PWA Excalidraw, in modo che tu possa trarre ispirazione da queste idee e applicarle alle tue app.
Come sono arrivato a Excalidraw
Voglio iniziare con una storia. Il 1° gennaio 2020, Christopher Chedeau, un software engineer di Facebook, ha twittato di una piccola app di disegno su cui aveva iniziato a lavorare. Con questo strumento, puoi disegnare caselle e frecce che sembrano fumetti e disegni a mano. Il giorno successivo, potevi anche disegnare ellissi e testo, nonché selezionare oggetti e spostarli. Il 3 gennaio, l'app ha ricevuto il nome Excalidraw e, come per ogni buon progetto secondario, l'acquisto del nome di dominio è stato uno dei primi atti di Christopher. A questo punto, puoi utilizzare i colori ed esportare l'intero disegno come PNG.

Il 15 gennaio, Christopher ha pubblicato un blog post che ha attirato molta attenzione su Twitter, compresa la mia. Il post iniziava con alcune statistiche impressionanti:
- 12.000 utenti attivi unici
- 1500 stelle su GitHub
- 26 collaboratori
Per un progetto iniziato solo due settimane fa, non è affatto male. Ma ciò che ha davvero suscitato il mio interesse era più in basso nel post. Christopher ha scritto di aver provato qualcosa di nuovo questa volta: concedere l'accesso incondizionato ai commit a tutti coloro che hanno inviato una richiesta di pull. Lo stesso giorno in cui ho letto il blog post, ho inviato una richiesta di pull che aggiungeva il supporto dell'API File System Access a Excalidraw, risolvendo una richiesta di funzionalità che qualcuno aveva presentato.

La mia richiesta di pull è stata unita un giorno dopo e da quel momento ho avuto accesso completo ai commit. Inutile dire che non ho abusato del mio potere. E nemmeno nessun altro dei 149 collaboratori finora.
Oggi Excalidraw è un'app web progressiva installabile a tutti gli effetti con supporto offline, una straordinaria modalità Buio e, sì, la possibilità di aprire e salvare file grazie all'API File System Access.

Lipis sul motivo per cui dedica così tanto tempo a Excalidraw
Questo segna la fine della mia storia su "come sono arrivato a Excalidraw", ma prima di approfondire alcune delle straordinarie funzionalità di Excalidraw, ho il piacere di presentare Panayiotis. Panayiotis Lipiridis, noto su internet semplicemente come lipis, è il collaboratore più prolifico di Excalidraw. Ho chiesto a lipis cosa lo spinge a dedicare così tanto tempo a Excalidraw:
Come tutti, ho saputo di questo progetto dal tweet di Christopher. Il mio primo contributo è stato l'aggiunta della libreria Open Color, i colori che fanno ancora parte di Excalidraw oggi. Man mano che il progetto cresceva e ricevevamo molte richieste, il mio successivo grande contributo è stato quello di creare un backend per archiviare i disegni in modo che gli utenti potessero condividerli. Ma ciò che mi spinge davvero a contribuire è che chiunque abbia provato Excalidraw cerca scuse per usarlo di nuovo.
Sono pienamente d'accordo con lipis. Chiunque abbia provato Excalidraw cerca scuse per usarlo di nuovo.
Excalidraw in azione
Ora voglio mostrarti come puoi utilizzare Excalidraw nella pratica. Non sono un grande artista, ma il logo di Google I/O è abbastanza semplice, quindi ci provo. Un riquadro è la "i", una linea può essere la barra e la "o" è un cerchio. Tengo premuto Maiusc per ottenere un cerchio perfetto. Sposto un po' lo slash per renderlo più visibile. Ora un po' di colore per la "i" e la "o". Il blu è un buon segno. Forse uno stile di riempimento diverso? Tutto pieno o tratteggio incrociato? No, il tratteggio è fantastico. Non è perfetto, ma questa è l'idea di Excalidraw, quindi lasciatemi salvare.
Faccio clic sull'icona di salvataggio e inserisco un nome file nella finestra di dialogo di salvataggio del file. In Chrome, un browser che supporta l'API File System Access, non si tratta di un download, ma di una vera e propria operazione di salvataggio, in cui posso scegliere la posizione e il nome del file e, se apporto modifiche, posso salvarle nello stesso file.
Cambio il logo e rendo rossa la "i". Se ora faccio di nuovo clic su Salva, la modifica viene salvata nello stesso file di prima. Per dimostrarlo, cancello la tela e riapro il file. Come puoi vedere, il logo rosso-blu modificato è di nuovo presente.
Utilizzo dei file
Sui browser che attualmente non supportano l'API File System Access, ogni operazione di salvataggio è un download, quindi quando apporto modifiche, mi ritrovo con più file con un numero incrementale nel nome file che riempiono la mia cartella Download. Tuttavia, nonostante questo svantaggio, posso comunque salvare il file.
Apertura dei file
Qual è il segreto? Come possono funzionare l'apertura e il salvataggio su browser diversi che potrebbero o meno supportare l'API File System Access? L'apertura di un file in Excalidraw avviene in una funzione chiamata loadFromJSON)(), che a sua volta chiama una funzione chiamata fileOpen().
export const loadFromJSON = async (localAppState: AppState) => {
const blob = await fileOpen({
description: 'Excalidraw files',
extensions: ['.json', '.excalidraw', '.png', '.svg'],
mimeTypes: ['application/json', 'image/png', 'image/svg+xml'],
});
return loadFromBlob(blob, localAppState);
};
La funzione fileOpen() proviene da una piccola libreria che ho scritto, chiamata browser-fs-access, che utilizziamo in Excalidraw. Questa libreria fornisce l'accesso al file system tramite l'API File System Access con un fallback legacy, in modo che possa essere utilizzata in qualsiasi browser.
Innanzitutto, ti mostrerò l'implementazione per i casi in cui l'API è supportata. Dopo aver negoziato i tipi MIME e le estensioni dei file accettati, la parte centrale chiama la funzione showOpenFilePicker() dell'API File System Access. Questa funzione restituisce un array di file o un singolo file, a seconda che siano selezionati più file. A questo punto, non resta che inserire l'handle del file nell'oggetto file, in modo che possa essere recuperato di nuovo.
export default async (options = {}) => {
const accept = {};
// Not shown: deal with extensions and MIME types.
const handleOrHandles = await window.showOpenFilePicker({
types: [
{
description: options.description || '',
accept: accept,
},
],
multiple: options.multiple || false,
});
const files = await Promise.all(handleOrHandles.map(getFileWithHandle));
if (options.multiple) return files;
return files[0];
const getFileWithHandle = async (handle) => {
const file = await handle.getFile();
file.handle = handle;
return file;
};
};
L'implementazione di riserva si basa su un elemento input di tipo "file". Dopo la negoziazione dei tipi MIME e delle estensioni da accettare, il passaggio successivo consiste nel fare clic in modo programmatico sull'elemento di input in modo che venga visualizzata la finestra di dialogo di apertura del file. In caso di modifica, ovvero quando l'utente ha selezionato uno o più file, la promessa viene risolta.
export default async (options = {}) => {
return new Promise((resolve) => {
const input = document.createElement('input');
input.type = 'file';
const accept = [
...(options.mimeTypes ? options.mimeTypes : []),
options.extensions ? options.extensions : [],
].join();
input.multiple = options.multiple || false;
input.accept = accept || '*/*';
input.addEventListener('change', () => {
resolve(input.multiple ? Array.from(input.files) : input.files[0]);
});
input.click();
});
};
Salvataggio dei file in corso…
Ora passiamo al risparmio. In Excalidraw, il salvataggio avviene in una funzione chiamata saveAsJSON(). Innanzitutto, serializza l'array di elementi Excalidraw in JSON, converte il JSON in un blob e poi chiama una funzione denominata fileSave(). Anche questa funzione è fornita dalla libreria browser-fs-access.
export const saveAsJSON = async (
elements: readonly ExcalidrawElement[],
appState: AppState,
) => {
const serialized = serializeAsJSON(elements, appState);
const blob = new Blob([serialized], {
type: 'application/vnd.excalidraw+json',
});
const fileHandle = await fileSave(
blob,
{
fileName: appState.name,
description: 'Excalidraw file',
extensions: ['.excalidraw'],
},
appState.fileHandle,
);
return { fileHandle };
};
Ancora una volta, diamo un'occhiata all'implementazione per i browser con supporto dell'API File System Access. Le prime righe sembrano un po' complesse, ma si limitano a negoziare i tipi MIME e le estensioni dei file. Se ho già salvato in precedenza e ho già un handle di file, non è necessario mostrare alcuna finestra di dialogo di salvataggio. Se invece si tratta del primo salvataggio, viene visualizzata una finestra di dialogo del file e l'app riceve un handle di file per un utilizzo futuro. Il resto consiste semplicemente nella scrittura nel file, che avviene tramite uno stream scrivibile.
export default async (blob, options = {}, handle = null) => {
options.fileName = options.fileName || 'Untitled';
const accept = {};
// Not shown: deal with extensions and MIME types.
handle =
handle ||
(await window.showSaveFilePicker({
suggestedName: options.fileName,
types: [
{
description: options.description || '',
accept: accept,
},
],
}));
const writable = await handle.createWritable();
await writable.write(blob);
await writable.close();
return handle;
};
La funzionalità "Salva con nome"
Se decido di ignorare un handle di file già esistente, posso implementare una funzionalità "Salva con nome" per creare un nuovo file basato su un file esistente. Per dimostrarlo, aprirò un file esistente, apporterò alcune modifiche e, anziché sovrascrivere il file esistente, creerò un nuovo file utilizzando la funzionalità Salva con nome. In questo modo, il file originale rimane invariato.
L'implementazione per i browser che non supportano l'API File System Access è breve, poiché si limita a creare un elemento anchor con un attributo download il cui valore è il nome file desiderato e un URL blob come valore dell'attributo href.
export default async (blob, options = {}) => {
const a = document.createElement('a');
a.download = options.fileName || 'Untitled';
a.href = URL.createObjectURL(blob);
a.addEventListener('click', () => {
setTimeout(() => URL.revokeObjectURL(a.href), 30 * 1000);
});
a.click();
};
L'elemento di ancoraggio viene quindi selezionato in modo programmatico. Per evitare perdite di memoria, l'URL del blob deve essere revocato dopo l'uso. Poiché si tratta solo di un download, non viene mai visualizzata una finestra di dialogo per il salvataggio dei file e tutti i file vengono inseriti nella cartella Downloads predefinita.
Trascina
Una delle mie integrazioni di sistema preferite su computer è il trascinamento. In Excalidraw, quando trascino un file .excalidraw nell'applicazione, questo si apre immediatamente e posso iniziare a modificarlo. Sui browser che supportano l'API File System Access, posso anche salvare immediatamente le modifiche. Non è necessario passare attraverso una finestra di dialogo di salvataggio del file, poiché l'handle del file richiesto è stato ottenuto dall'operazione di trascinamento.
Il segreto per farlo è chiamare getAsFileSystemHandle() sull'elemento di trasferimento di dati quando l'API File System Access è supportata. Poi passo questo handle di file a loadFromBlob(), che potresti ricordare da un paio di paragrafi precedenti. Ci sono così tante cose che puoi fare con i file: aprirli, salvarli, salvarli di nuovo, trascinarli, rilasciarli. Io e il mio collega Pete abbiamo documentato tutti questi trucchi e altri ancora nel nostro articolo, così puoi recuperare nel caso in cui tutto questo sia stato un po' troppo veloce.
const file = event.dataTransfer?.files[0];
if (file?.type === 'application/json' || file?.name.endsWith('.excalidraw')) {
this.setState({ isLoading: true });
// Provided by browser-fs-access.
if (supported) {
try {
const item = event.dataTransfer.items[0];
file as any.handle = await item as any
.getAsFileSystemHandle();
} catch (error) {
console.warn(error.name, error.message);
}
}
loadFromBlob(file, this.state).then(({ elements, appState }) =>
// Load from blob
).catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
}
Condividere i file
Un'altra integrazione di sistema attualmente disponibile su Android, ChromeOS e Windows avviene tramite l'API Web Share Target. Ecco l'app Files nella mia cartella Downloads. Vedo due file, uno con il nome non descrittivo untitled e un timestamp. Per controllare cosa contiene, faccio clic sui tre puntini, poi su Condividi e una delle opzioni visualizzate è Excalidraw. Quando tocco l'icona, vedo che il file contiene di nuovo solo il logo di I/O.
Lipis sulla versione di Electron deprecata
Un'operazione che puoi eseguire con i file di cui non ho ancora parlato è fare doppio clic. In genere, quando fai doppio clic su un file, si apre l'app associata al tipo MIME del file. Ad esempio, per .docx si tratta di Microsoft Word.
Excalidraw aveva una versione Electron dell'app che supportava queste associazioni di tipi di file, quindi quando facevi doppio clic su un file .excalidraw, si apriva l'app Excalidraw Electron. Lipis, che hai già incontrato, è stato sia il creatore
sia il deprecator di Excalidraw Electron. Gli ho chiesto perché riteneva possibile ritirare la versione Electron:
Gli utenti chiedevano un'app Electron fin dall'inizio, principalmente perché volevano aprire i file facendo doppio clic. Inoltre, volevamo pubblicare l'app negli store. In parallelo, qualcuno ha suggerito di creare una PWA, quindi abbiamo fatto entrambe le cose. Fortunatamente, abbiamo scoperto le API di Project Fugu, come l'accesso al file system, l'accesso agli appunti, la gestione dei file e altro ancora. Con un solo clic puoi installare l'app sul tuo computer o dispositivo mobile, senza il peso aggiuntivo di Electron. È stato facile decidere di ritirare la versione Electron, concentrarsi solo sull'app web e renderla la migliore PWA possibile. Inoltre, ora possiamo pubblicare le PWA sul Play Store e sul Microsoft Store. È un risultato enorme!
Si potrebbe dire che Excalidraw per Electron non è stato ritirato perché Electron è un cattivo strumento, tutt'altro, ma perché il web è diventato abbastanza buono. Mi piace!
Gestione di file
Quando dico che "il web è diventato abbastanza buono", è grazie a funzionalità come la gestione dei file, che sarà disponibile a breve.
Si tratta di una normale installazione di macOS Big Sur. Ora vediamo cosa succede quando faccio clic con il tasto destro del mouse su un file Excalidraw. Posso scegliere di aprirlo con Excalidraw, la PWA installata. Naturalmente, anche fare doppio clic funzionerebbe, ma è meno d'effetto da mostrare in uno screencast.
Come funziona? Il primo passaggio consiste nel far conoscere al sistema operativo i tipi di file che la mia applicazione può gestire. Per farlo, utilizzo un nuovo campo chiamato file_handlers nel file manifest dell'app web. Il suo valore è un array di oggetti con un'azione e una proprietà accept. L'azione determina il percorso dell'URL in cui il sistema operativo avvia l'app e l'oggetto accept è costituito da coppie chiave-valore di tipi MIME e dalle estensioni del file associate.
{
"name": "Excalidraw",
"description": "Excalidraw is a whiteboard tool...",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"file_handlers": [
{
"action": "/",
"accept": {
"application/vnd.excalidraw+json": [".excalidraw"]
}
}
]
}
Il passaggio successivo consiste nel gestire il file all'avvio dell'applicazione. Questo avviene nell'interfaccia launchQueue, dove devo impostare un consumer chiamando setConsumer(). Il parametro di questa funzione è una funzione asincrona che riceve launchParams. Questo oggetto launchParams ha un campo chiamato files che mi fornisce un array di handle di file con cui lavorare. Mi interessa solo il primo e da questo handle di file ottengo un blob che poi passo al nostro vecchio amico loadFromBlob().
if ('launchQueue' in window && 'LaunchParams' in window) {
window as any.launchQueue
.setConsumer(async (launchParams: { files: any[] }) => {
if (!launchParams.files.length) return;
const fileHandle = launchParams.files[0];
const blob: Blob = await fileHandle.getFile();
blob.handle = fileHandle;
loadFromBlob(blob, this.state).then(({ elements, appState }) =>
// Initialize app state.
).catch((error) => {
this.setState({ isLoading: false, errorMessage: error.message });
});
});
}
Se questo passaggio è stato troppo veloce, puoi scoprire di più sull'API Gestione di file nel mio articolo. Puoi attivare la gestione dei file impostando il flag delle funzionalità sperimentali della piattaforma web. Il lancio in Chrome è previsto per la fine dell'anno.
Integrazione degli appunti
Un'altra funzionalità interessante di Excalidraw è l'integrazione con gli appunti. Posso copiare l'intero disegno o solo parti di esso negli appunti, magari aggiungendo una filigrana se voglio, e poi incollarlo in un'altra app. A proposito, questa è una versione web dell'app Paint di Windows 95.
Il funzionamento è sorprendentemente semplice. Tutto ciò di cui ho bisogno è il canvas come blob, che poi scrivo negli appunti passando un array di un elemento con un ClipboardItem con il blob alla funzione navigator.clipboard.write(). Per ulteriori informazioni su cosa puoi fare con l'API Clipboard, consulta l'articolo di Jason e il mio articolo.
export const copyCanvasToClipboardAsPng = async (canvas: HTMLCanvasElement) => {
const blob = await canvasToBlob(canvas);
await navigator.clipboard.write([
new window.ClipboardItem({
'image/png': blob,
}),
]);
};
export const canvasToBlob = async (canvas: HTMLCanvasElement): Promise<Blob> => {
return new Promise((resolve, reject) => {
try {
canvas.toBlob((blob) => {
if (!blob) {
return reject(new CanvasError(t('canvasError.canvasTooBig'), 'CANVAS_POSSIBLY_TOO_BIG'));
}
resolve(blob);
});
} catch (error) {
reject(error);
}
});
};
Collaborazione con altri utenti
Condivisione dell'URL di una sessione
Sapevi che Excalidraw ha anche una modalità di collaborazione? Persone diverse possono collaborare allo stesso documento. Per avviare una nuova sessione, faccio clic sul pulsante di collaborazione dal vivo e poi avvio una sessione. Posso condividere facilmente l'URL della sessione con i miei collaboratori grazie all'API Web Share integrata in Excalidraw.
Collaborazione dal vivo
Ho simulato una sessione di collaborazione locale lavorando sul logo di Google I/O sul mio Pixelbook, sul mio smartphone Pixel 3a e sul mio iPad Pro. Come puoi vedere, le modifiche che apporto su un dispositivo vengono applicate su tutti gli altri dispositivi.
Posso persino vedere tutti i cursori che si muovono. Il cursore di Pixelbook si muove in modo costante, poiché è controllato da un trackpad, ma il cursore dello smartphone Pixel 3a e quello del tablet iPad Pro saltano, poiché controllo questi dispositivi toccandoli con il dito.
Visualizzazione degli stati dei collaboratori
Per migliorare l'esperienza di collaborazione in tempo reale, è anche in esecuzione un sistema di rilevamento dell'inattività. Il cursore dell'iPad Pro mostra un punto verde quando lo utilizzo. Il punto diventa nero quando passo a un'altra scheda del browser o a un'altra app. Quando sono nell'app Excalidraw, ma non sto facendo nulla, il cursore mostra che sono inattivo, con il simbolo delle tre zZZ.
I lettori più attenti delle nostre pubblicazioni potrebbero essere inclini a pensare che il rilevamento dell'inattività venga realizzato tramite l'API Idle Detection, una proposta in fase iniziale su cui si sta lavorando nell'ambito del progetto Fugu. Spoiler: non lo è. Sebbene avessimo un'implementazione basata su questa API in Excalidraw, alla fine abbiamo deciso di adottare un approccio più tradizionale basato sulla misurazione del movimento del puntatore e della visibilità della pagina.

Abbiamo inviato un feedback sul motivo per cui l'API Idle Detection non risolveva il nostro caso d'uso. Tutte le API Project Fugu vengono sviluppate in modo aperto, quindi tutti possono intervenire e far sentire la propria voce.
Lipis on what is holding back Excalidraw
A questo proposito, ho posto a lipis un'ultima domanda su ciò che secondo lui manca alla piattaforma web che frena Excalidraw:
L'API File System Access è fantastica, ma sai una cosa? La maggior parte dei file che mi interessano oggi si trova su Dropbox o Google Drive, non sul mio disco rigido. Vorrei che l'API File System Access includesse un livello di astrazione per i provider di file system remoti come Dropbox o Google, con cui gli sviluppatori potessero integrarsi e scrivere codice. Gli utenti potevano quindi rilassarsi e sapere che i loro file erano al sicuro con il provider cloud di cui si fidavano.
Sono pienamente d'accordo con lipis, anch'io vivo nel cloud. Speriamo che questa funzionalità venga implementata presto.
Modalità applicazione a schede
Wow! Abbiamo visto molte integrazioni API davvero fantastiche in Excalidraw. File system, gestione dei file, appunti, condivisione web e destinazione della condivisione web. Ma c'è un'altra cosa. Fino ad ora, potevo modificare un solo documento alla volta. Non più. Per la prima volta, goditi una versione iniziale della modalità di applicazione a schede in Excalidraw. Ecco come appare.
Ho un file esistente aperto nella PWA Excalidraw installata, che è in esecuzione in modalità autonoma. Ora apro una nuova scheda nella finestra autonoma. Non si tratta di una normale scheda del browser, ma di una scheda PWA. In questa nuova scheda posso aprire un file secondario e lavorarci in modo indipendente dalla stessa finestra dell'app.
La modalità applicazione con schede è nelle sue fasi iniziali e non tutto è definitivo. Se ti interessa, assicurati di leggere lo stato attuale di questa funzionalità nel mio articolo.
Chiusura
Per rimanere al corrente di questa e di altre funzionalità, assicurati di consultare il nostro tracker delle API Fugu. Siamo felici di far progredire il web e di consentirti di fare di più sulla piattaforma. Ci auguriamo che Excalidraw continui a migliorare e che tu possa creare applicazioni straordinarie. Inizia a creare su excalidraw.com.
Non vedo l'ora di vedere alcune delle API che ho mostrato oggi nelle tue app. Mi chiamo Tom, puoi trovarmi come @tomayac su Twitter e su internet in generale. Grazie mille per l'attenzione e buon proseguimento di Google I/O.