Jede hinreichend fortgeschrittene Technologie ist von Magie nicht mehr zu unterscheiden. Es sei denn, Sie verstehen es. Mein Name ist Thomas Steiner. Ich arbeite bei Google im Bereich Developer Relations. In diesem Beitrag zu meinem Google I/O-Vortrag stelle ich einige der neuen Fugu-APIs vor und zeige, wie sie die wichtigsten Nutzeraktionen in der Excalidraw-PWA verbessern. Sie können sich von diesen Ideen inspirieren lassen und sie auf Ihre eigenen Apps anwenden.
Wie ich zu Excalidraw gekommen bin
Ich möchte mit einer Geschichte beginnen. Am 1. Januar 2020 twitterte Christopher Chedeau, ein Softwareentwickler bei Facebook, über eine kleine Zeichen-App, an der er zu arbeiten begonnen hatte. Mit diesem Tool konnten Sie Kästen und Pfeile zeichnen, die cartoonartig und handgezeichnet wirkten. Am nächsten Tag konnten Sie auch Ellipsen und Text zeichnen sowie Objekte auswählen und verschieben. Am 3. Januar erhielt die App den Namen Excalidraw und wie bei jedem guten Nebenprojekt war der Kauf des Domainnamens eine der ersten Handlungen von Christopher. Sie konnten bereits Farben verwenden und die gesamte Zeichnung als PNG exportieren.

Am 15. Januar veröffentlichte Christopher einen Blogbeitrag, der auf Twitter viel Aufmerksamkeit erregte, auch bei mir. Der Beitrag begann mit einigen beeindruckenden Statistiken:
- 12.000 einzelne aktive Nutzer
- 1.500 Sterne auf GitHub
- 26 Beitragende
Für ein Projekt, das erst vor zwei Wochen gestartet wurde, ist das gar nicht schlecht. Was mein Interesse aber wirklich geweckt hat, war weiter unten im Beitrag. Christopher schrieb, dass er dieses Mal etwas Neues ausprobiert hat: Er hat allen, die einen Pull-Request eingereicht haben, uneingeschränkten Commit-Zugriff gewährt. Am selben Tag, an dem ich den Blogpost gelesen hatte, habe ich einen Pull-Request erstellt, mit dem die File System Access API in Excalidraw unterstützt wird. Damit wurde eine Funktionsanfrage behoben, die jemand gestellt hatte.

Meine Pull-Anfrage wurde einen Tag später zusammengeführt und von da an hatte ich vollen Commit-Zugriff. Ich habe meine Macht natürlich nicht missbraucht. Das gilt auch für alle anderen der bisher 149 Beitragenden.
Heute ist Excalidraw eine vollwertige installierbare Progressive Web-App mit Offlineunterstützung, einem beeindruckenden Dark Mode und der Möglichkeit, Dateien zu öffnen und zu speichern, dank der File System Access API.

Lipis erklärt, warum er so viel Zeit in Excalidraw investiert
Das war die Geschichte, wie ich zu Excalidraw gekommen bin. Bevor ich auf einige der tollen Funktionen von Excalidraw eingehe, möchte ich Panayiotis vorstellen. Panayiotis Lipiridis, im Internet einfach als lipis bekannt, ist der produktivste Mitwirkende bei Excalidraw. Ich habe lipis gefragt, was ihn motiviert, so viel Zeit in Excalidraw zu investieren:
Wie alle anderen habe ich durch Christophers Tweet von diesem Projekt erfahren. Mein erster Beitrag war das Hinzufügen der Open Color-Bibliothek, deren Farben bis heute Teil von Excalidraw sind. Als das Projekt wuchs und wir viele Anfragen erhielten, war mein nächster großer Beitrag die Entwicklung eines Backends zum Speichern von Zeichnungen, damit Nutzer sie teilen konnten. Was mich aber wirklich dazu motiviert, etwas beizutragen, ist, dass jeder, der Excalidraw ausprobiert hat, nach Ausreden sucht, es wieder zu verwenden.
Ich stimme lipis voll und ganz zu. Wer Excalidraw ausprobiert hat, sucht nach Gründen, es wieder zu verwenden.
Excalidraw in Aktion
Ich möchte Ihnen jetzt zeigen, wie Sie Excalidraw in der Praxis verwenden können. Ich bin kein großer Künstler, aber das Google I/O-Logo ist einfach genug. Ich versuche es mal. Ein Kästchen ist das „i“, eine Linie kann der Schrägstrich sein und das „o“ ist ein Kreis. Ich halte die Umschalttaste gedrückt, damit ich einen perfekten Kreis erhalte. Ich verschiebe den Schrägstrich ein wenig, damit es besser aussieht. Jetzt noch etwas Farbe für das „i“ und das „o“. Blau ist gut. Vielleicht ein anderer Füllstil? Voll oder schraffiert? Nein, die Schraffur sieht gut aus. Es ist nicht perfekt, aber das ist die Idee von Excalidraw. Ich speichere es trotzdem.
Ich klicke auf das Symbol zum Speichern und gebe im Dialogfeld zum Speichern von Dateien einen Dateinamen ein. In Chrome, einem Browser, der die File System Access API unterstützt, ist dies kein Download, sondern ein echter Speichervorgang. Ich kann den Speicherort und den Namen der Datei auswählen und Änderungen einfach in derselben Datei speichern.
Ich ändere das Logo und mache das „i“ rot. Wenn ich jetzt noch einmal auf „Speichern“ klicke, wird meine Änderung in derselben Datei wie zuvor gespeichert. Als Beweis lösche ich den Inhalt der Arbeitsfläche und öffne die Datei noch einmal. Wie Sie sehen, ist das modifizierte rot-blaue Logo wieder da.
Mit Dateien arbeiten
In Browsern, die die File System Access API derzeit nicht unterstützen, ist jeder Speichervorgang ein Download. Wenn ich also Änderungen vornehme, erhalte ich mehrere Dateien mit einer fortlaufenden Nummer im Dateinamen, die meinen Downloadordner füllen. Trotz dieses Nachteils kann ich die Datei aber trotzdem speichern.
Dateien öffnen
Was ist also das Geheimnis? Wie kann das Öffnen und Speichern in verschiedenen Browsern funktionieren, die die File System Access API möglicherweise unterstützen oder nicht? Das Öffnen einer Datei in Excalidraw erfolgt in einer Funktion namens loadFromJSON)(, die wiederum eine Funktion namens fileOpen() aufruft.
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);
};
Die Funktion fileOpen() stammt aus einer kleinen Bibliothek, die ich geschrieben habe und die browser-fs-access heißt. Wir verwenden sie in Excalidraw. Diese Bibliothek bietet Dateisystemzugriff über die File System Access API mit einem Legacy-Fallback, sodass sie in jedem Browser verwendet werden kann.
Zuerst zeige ich Ihnen die Implementierung für den Fall, dass die API unterstützt wird. Nachdem die akzeptierten MIME-Typen und Dateiendungen ausgehandelt wurden, wird die Funktion showOpenFilePicker() der File System Access API aufgerufen. Diese Funktion gibt ein Array von Dateien oder eine einzelne Datei zurück, je nachdem, ob mehrere Dateien ausgewählt sind. Jetzt muss nur noch das Dateihandle in das Dateiobjekt eingefügt werden, damit es wieder abgerufen werden kann.
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;
};
};
Die Fallback-Implementierung basiert auf einem input-Element vom Typ "file". Nach der Aushandlung der zu akzeptierenden MIME-Typen und Erweiterungen besteht der nächste Schritt darin, programmatisch auf das Eingabeelement zu klicken, damit das Dialogfeld zum Öffnen von Dateien angezeigt wird. Das Promise wird aufgelöst, wenn der Nutzer eine oder mehrere Dateien ausgewählt hat.
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();
});
};
Dateien speichern
Jetzt zum Sparen. In Excalidraw erfolgt das Speichern in einer Funktion namens saveAsJSON(). Zuerst wird das Excalidraw-Elementarray in JSON serialisiert, dann wird das JSON in einen Blob konvertiert und schließlich wird eine Funktion namens fileSave() aufgerufen. Diese Funktion wird ebenfalls von der Bibliothek browser-fs-access bereitgestellt.
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 };
};
Sehen wir uns zuerst die Implementierung für Browser mit Unterstützung der File System Access API an. Die ersten Zeilen sind etwas kompliziert, aber sie dienen nur dazu, die MIME-Typen und Dateinamenerweiterungen zu vereinbaren. Wenn ich bereits gespeichert habe und bereits ein Dateihandle habe, muss kein Speicherdialogfeld angezeigt werden. Wenn es sich jedoch um das erste Speichern handelt, wird ein Dateidialogfeld angezeigt und die App erhält einen Dateihandle zur zukünftigen Verwendung zurück. Der Rest besteht dann nur noch darin, in die Datei zu schreiben, was über einen schreibbaren Stream erfolgt.
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;
};
Die Funktion „Speichern unter“
Wenn ich mich entscheide, ein bereits vorhandenes Dateihandle zu ignorieren, kann ich eine „Speichern unter“-Funktion implementieren, um eine neue Datei auf Grundlage einer vorhandenen Datei zu erstellen. Um das zu zeigen, öffne ich eine vorhandene Datei, nehme einige Änderungen vor und überschreibe dann nicht die vorhandene Datei, sondern erstelle mit der Funktion „Speichern unter“ eine neue Datei. Die Originaldatei bleibt dabei erhalten.
Die Implementierung für Browser, die die File System Access API nicht unterstützen, ist kurz, da sie nur ein Anker-Element mit einem download-Attribut erstellt, dessen Wert der gewünschte Dateiname ist, und eine Blob-URL als href-Attributwert.
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();
};
Das Anker-Element wird dann programmatisch angeklickt. Um Speicherlecks zu vermeiden, muss die Blob-URL nach der Verwendung widerrufen werden. Da es sich nur um einen Download handelt, wird nie ein Dialogfeld zum Speichern von Dateien angezeigt und alle Dateien werden im Standardordner Downloads gespeichert.
Drag-and-Drop
Eine meiner Lieblingssystemintegrationen auf dem Desktop ist Drag-and-drop. Wenn ich in Excalidraw eine .excalidraw-Datei in die Anwendung ziehe, wird sie sofort geöffnet und ich kann mit der Bearbeitung beginnen. In Browsern, die die File System Access API unterstützen, kann ich meine Änderungen dann sogar sofort speichern. Da der erforderliche Dateihandle durch das Ziehen und Ablegen abgerufen wurde, ist kein Dialogfeld zum Speichern der Datei erforderlich.
Das Geheimnis besteht darin, getAsFileSystemHandle() für das Element data transfer aufzurufen, wenn die File System Access API unterstützt wird. Ich übergebe diesen Dateihandle dann an loadFromBlob(), das Sie vielleicht noch von einigen Absätzen weiter oben kennen. Mit Dateien können Sie vieles tun: öffnen, speichern, überschreiben, ziehen und ablegen. Mein Kollege Pete und ich haben alle diese Tricks und mehr in unserem Artikel dokumentiert. Dort können Sie alles nachlesen, falls es Ihnen zu schnell ging.
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 });
});
}
Dateien teilen
Eine weitere Systemintegration, die derzeit unter Android, ChromeOS und Windows verfügbar ist, erfolgt über die Web Share Target API. Hier bin ich in der Dateimanager-App im Ordner Downloads. Ich sehe zwei Dateien, eine davon mit dem nichtssagenden Namen untitled und einem Zeitstempel. Um zu sehen, was es enthält, klicke ich auf die drei Punkte, dann auf „Teilen“ und eine der angezeigten Optionen ist Excalidraw. Wenn ich auf das Symbol tippe, sehe ich, dass die Datei nur das I/O-Logo enthält.
Lipis in der verworfenen Electron-Version
Eine Sache, die Sie mit Dateien tun können, die ich noch nicht erwähnt habe, ist, darauf zu doppelklicken. Wenn Sie normalerweise auf eine Datei doppelklicken, wird die App geöffnet, die dem MIME-Typ der Datei zugeordnet ist. Für .docx wäre das beispielsweise Microsoft Word.
Früher gab es eine Electron-Version von Excalidraw, die solche Dateitypzuordnungen unterstützte. Wenn Sie also auf eine .excalidraw-Datei doppelklickten, wurde die Excalidraw Electron-App geöffnet. Lipis, den Sie bereits kennengelernt haben, war sowohl der Entwickler als auch derjenige, der Excalidraw Electron eingestellt hat. Ich fragte ihn, warum er der Meinung war, dass die Electron-Version eingestellt werden könnte:
Seit Beginn der Entwicklung von Google Drive für den Computer haben sich Nutzer eine Electron-App gewünscht, vor allem, weil sie Dateien durch Doppelklicken öffnen wollten. Wir wollten die App auch in App-Stores anbieten. Gleichzeitig schlug jemand vor, stattdessen eine PWA zu erstellen, also haben wir einfach beides gemacht. Glücklicherweise wurden wir mit Project Fugu-APIs wie Dateisystemzugriff, Zwischenablagezugriff und Dateiverarbeitung vertraut gemacht. Mit nur einem Klick können Sie die App auf Ihrem Computer oder Mobilgerät installieren, ohne die zusätzliche Größe von Electron. Es war eine einfache Entscheidung, die Electron-Version einzustellen, sich nur auf die Web-App zu konzentrieren und sie zur bestmöglichen PWA zu machen. Außerdem können wir PWAs jetzt im Play Store und im Microsoft Store veröffentlichen. Das ist ein enormer Anstieg.
Man könnte sagen, dass Excalidraw für Electron nicht eingestellt wurde, weil Electron schlecht ist, sondern weil das Web gut genug geworden ist. Das gefällt mir!
Dateiverwaltung
Wenn ich sage, dass das Web gut genug geworden ist, liegt das an Funktionen wie der bevorstehenden Dateiverarbeitung.
Dies ist eine reguläre Installation von macOS Big Sur. Sehen wir uns nun an, was passiert, wenn ich mit der rechten Maustaste auf eine Excalidraw-Datei klicke. Ich kann sie mit Excalidraw, der installierten PWA, öffnen. Natürlich funktioniert auch ein Doppelklick, aber in einem Screencast ist das weniger eindrucksvoll.
Wie funktioniert das? Im ersten Schritt müssen Sie dem Betriebssystem die Dateitypen mitteilen, die Ihre Anwendung verarbeiten kann. Ich verwende dazu ein neues Feld namens file_handlers im Web-App-Manifest. Sein Wert ist ein Array von Objekten mit einer Aktion und einer accept-Property. Die Aktion bestimmt den URL-Pfad, unter dem das Betriebssystem Ihre App startet. Das Accept-Objekt besteht aus Schlüssel/Wert-Paaren von MIME-Typen und den zugehörigen Dateiendungen.
{
"name": "Excalidraw",
"description": "Excalidraw is a whiteboard tool...",
"start_url": "/",
"display": "standalone",
"theme_color": "#000000",
"background_color": "#ffffff",
"file_handlers"