Toute technologie suffisamment avancée est indiscernable de la magie. Sauf si vous le comprenez. Je m'appelle Thomas Steiner et je travaille dans les relations avec les développeurs chez Google. Dans cet article, qui reprend mon intervention lors de la Google I/O, je vais vous présenter certaines des nouvelles API Fugu et la façon dont elles améliorent les parcours utilisateur principaux dans la PWA Excalidraw. Vous pourrez ainsi vous inspirer de ces idées et les appliquer à vos propres applications.
Comment j'ai découvert Excalidraw
Je vais commencer par une histoire. Le 1er janvier 2020, Christopher Chedeau, ingénieur logiciel chez Facebook, a tweeté à propos d'une petite application de dessin sur laquelle il avait commencé à travailler. Avec cet outil, vous pouvez dessiner des cases et des flèches qui semblent dessinées à la main et dans un style cartoon. Le lendemain, vous pouvez également dessiner des ellipses et du texte, ainsi que sélectionner des objets et les déplacer. Le 3 janvier, l'application a reçu son nom, Excalidraw. Comme pour tout bon projet secondaire, l'achat du nom de domaine a été l'un des premiers actes de Christopher. Vous pouvez maintenant utiliser des couleurs et exporter l'ensemble du dessin au format PNG.
Le 15 janvier, Christopher a publié un article de blog qui a attiré beaucoup d'attention sur Twitter, y compris la mienne. Le post commence par des statistiques impressionnantes:
- 12 000 utilisateurs actifs uniques
- 1 500 étoiles sur GitHub
- 26 contributeurs
Pour un projet qui a commencé il y a seulement deux semaines, ce n'est pas mal du tout. Mais ce qui a vraiment attisé mon intérêt se trouvait plus loin dans le post. Christopher a écrit qu'il avait essayé quelque chose de nouveau cette fois-ci: il a accordé à tous ceux qui ont envoyé une requête de tirage un accès aux commits inconditionnel. Le même jour où j'ai lu l'article de blog, j'ai ajouté une pull request qui a permis d'intégrer l'API File System Access à Excalidraw, corrigeant une demande de fonctionnalité que quelqu'un avait envoyée.
Ma demande d'extraction a été fusionnée un jour plus tard. À partir de ce moment-là, j'ai eu un accès complet aux commits. Inutile de dire que je n'ai pas abusé de mon pouvoir. Aucun des 149 contributeurs n'a fait de même jusqu'à présent.
Aujourd'hui, Excalidraw est une application Web progressive installable complète, avec une compatibilité hors connexion, un mode sombre époustouflant et, oui, la possibilité d'ouvrir et d'enregistrer des fichiers grâce à l'API File System Access.
Lipis explique pourquoi il consacre une grande partie de son temps à Excalidraw
C'est donc la fin de mon histoire "Comment j'ai découvert Excalidraw", mais avant de vous présenter certaines des fonctionnalités incroyables d'Excalidraw, j'ai le plaisir de vous présenter Panayiotis. Panayiotis Lipiridis, sur Internet simplement connu sous le nom de lipis, est le contributeur le plus prolifique d'Excalidraw. J'ai demandé à lipis ce qui le motive à consacrer autant de temps à Excalidraw:
Comme tout le monde, j'ai découvert ce projet grâce au tweet de Christopher. Ma première contribution a été d'ajouter la bibliothèque de couleurs Open, les couleurs qui font toujours partie d'Excalidraw aujourd'hui. À mesure que le projet évoluait et que nous recevions de nombreuses demandes, ma prochaine grande contribution a été de créer un backend pour stocker les dessins afin que les utilisateurs puissent les partager. Mais ce qui me motive vraiment à contribuer, c'est que quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.
Je suis entièrement d'accord avec lipis. Quiconque a essayé Excalidraw cherche des excuses pour l'utiliser à nouveau.
Excalidraw en action
Je vais maintenant vous montrer comment utiliser Excalidraw en pratique. Je ne suis pas un grand artiste, mais le logo Google I/O est assez simple. Je vais donc essayer. Un cadre correspond à la lettre "i", une ligne peut être la barre oblique, et le "o" est un cercle. Je maintiens la touche Maj enfoncée pour obtenir un cercle parfait. Je vais déplacer la barre oblique pour qu'elle soit mieux alignée. Ajoutez maintenant de la couleur aux lettres "i" et "o". Le bleu, c'est bien. Peut-être un style de remplissage différent ? Toutes en noir ou en hachures ? Non, les hachures sont parfaites. Ce n'est pas parfait, mais c'est l'idée d'Excalidraw. Je vais donc l'enregistrer.
Je clique sur l'icône d'enregistrement, puis saisis un nom de fichier dans la boîte de dialogue d'enregistrement. Dans Chrome, un navigateur compatible avec l'API File System Access, il ne s'agit pas d'un téléchargement, mais d'une véritable opération d'enregistrement, où je peux choisir l'emplacement et le nom du fichier, et où, si je modifie le fichier, je peux simplement l'enregistrer dans le même fichier.
Je vais changer le logo et rendre l'i rouge. Si je clique à nouveau sur "Enregistrer", ma modification est enregistrée dans le même fichier qu'auparavant. Pour vous en convaincre, je vais effacer le canevas et rouvrir le fichier. Comme vous pouvez le voir, le logo rouge-bleu modifié est à nouveau présent.
Utiliser des fichiers
Sur les navigateurs qui ne sont pas compatibles avec l'API File System Access, chaque opération d'enregistrement est un téléchargement. Par conséquent, lorsque j'apporte des modifications, je me retrouve avec plusieurs fichiers dont le nom de fichier contient un numéro incrémentiel qui remplit mon dossier de téléchargements. Mais malgré cet inconvénient, je peux toujours enregistrer le fichier.
Ouvrir des fichiers
Quel est le secret ? Comment l'ouverture et l'enregistrement peuvent-ils fonctionner sur différents navigateurs qui peuvent ou non prendre en charge l'API File System Access ? L'ouverture d'un fichier dans Excalidraw se produit dans une fonction appelée loadFromJSON)(
, qui appelle à son tour une fonction appelée 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 fonction fileOpen()
provient d'une petite bibliothèque que j'ai écrite appelée browser-fs-access que nous utilisons dans Excalidraw. Cette bibliothèque fournit un accès au système de fichiers via l'API File System Access avec un ancien remplacement, ce qui lui permet d'être utilisée dans n'importe quel navigateur.
Je vais d'abord vous montrer l'implémentation lorsque l'API est prise en charge. Après avoir négocié les types MIME et les extensions de fichier acceptés, l'élément central consiste à appeler la fonction showOpenFilePicker()
de l'API File System Access. Cette fonction renvoie un tableau de fichiers ou un seul fichier, selon que plusieurs fichiers sont sélectionnés. Il ne reste plus qu'à placer le gestionnaire de fichiers sur l'objet de fichier afin qu'il puisse être récupéré à nouveau.
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'implémentation de remplacement repose sur un élément input
de type "file"
. Après la négociation des types et des extensions MIME à accepter, l'étape suivante consiste à cliquer de manière programmatique sur l'élément de saisie pour afficher la boîte de dialogue d'ouverture de fichier. En cas de modification, c'est-à-dire lorsque l'utilisateur a sélectionné un ou plusieurs fichiers, la promesse est résolue.
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();
});
};
Enregistrer des fichiers
Enregistrement Dans Excalidraw, l'enregistrement se produit dans une fonction appelée saveAsJSON()
. Elle sérialise d'abord le tableau d'éléments Excalidraw en JSON, convertit le JSON en blob, puis appelle une fonction appelée fileSave()
. Cette fonction est également fournie par la bibliothèque 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 };
};
Comme je l'ai déjà fait, commençons par l'implémentation pour les navigateurs compatibles avec l'API File System Access. Les deux premières lignes semblent un peu complexes, mais elles ne font que négocier les types MIME et les extensions de fichier. Lorsque j'ai déjà enregistré et que je dispose déjà d'un gestionnaire de fichiers, aucune boîte de dialogue d'enregistrement ne doit s'afficher. Toutefois, s'il s'agit de la première sauvegarde, une boîte de dialogue de fichier s'affiche et l'application récupère un gestionnaire de fichiers pour une utilisation ultérieure. Le reste consiste simplement à écrire dans le fichier, via un flux en écriture.
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;
};
Fonctionnalité "Enregistrer sous"
Si je décide d'ignorer un gestionnaire de fichiers déjà existant, je peux implémenter une fonctionnalité "Enregistrer sous" pour créer un fichier basé sur un fichier existant. Pour illustrer cela, laissez-moi ouvrir un fichier existant, apporter quelques modifications, puis ne pas écraser le fichier existant, mais créer un nouveau fichier à l'aide de la fonctionnalité "Enregistrer sous". Le fichier d'origine n'est pas altéré.
L'implémentation pour les navigateurs qui ne prennent pas en charge l'API File System Access est courte, car elle ne crée qu'un élément d'ancrage avec un attribut download
dont la valeur est le nom de fichier souhaité et une URL de blob comme valeur de l'attribut 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'élément d'ancrage est ensuite cliqué de manière programmatique. Pour éviter les fuites de mémoire, l'URL du blob doit être révoquée après utilisation. Comme il ne s'agit que d'un téléchargement, aucune boîte de dialogue d'enregistrement de fichier ne s'affiche jamais, et tous les fichiers se retrouvent dans le dossier Downloads
par défaut.
Glisser-déposer
L'une de mes intégrations système préférées sur ordinateur est le glisser-déposer. Dans Excalidraw, lorsque je dépose un fichier .excalidraw
sur l'application, il s'ouvre immédiatement et je peux commencer à le modifier. Sur les navigateurs compatibles avec l'API File System Access, je peux même enregistrer immédiatement mes modifications. Il n'est pas nécessaire d'utiliser une boîte de dialogue d'enregistrement de fichier, car le gestionnaire de fichiers requis a été obtenu à partir de l'opération de glisser-déposer.
Pour ce faire, appelez getAsFileSystemHandle()
sur l'élément de transfert de données lorsque l'API File System Access est prise en charge. Je transmets ensuite ce gestionnaire de fichiers à loadFromBlob()
, comme vous vous en souvenez peut-être dans quelques paragraphes ci-dessus. Vous pouvez faire beaucoup de choses avec les fichiers: les ouvrir, les enregistrer, les enregistrer de manière excessive, les faire glisser, les déposer, etc. Mon collègue Pete et moi-même avons documenté toutes ces astuces et plus encore dans notre article afin que vous puissiez vous rattraper au cas où tout cela serait passé un peu trop vite.
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 });
});
}
Partager des fichiers
Une autre intégration système est actuellement disponible sur Android, ChromeOS et Windows via l'API Web Share Target. Je suis dans l'application Fichiers, dans mon dossier Downloads
. Je vois deux fichiers, dont l'un porte le nom non descriptif untitled
et un code temporel. Pour vérifier son contenu, je clique sur les trois points, puis sur "Partager". L'une des options qui s'affiche est "Excalidraw". Lorsque j'appuie sur l'icône, je constate que le fichier ne contient à nouveau que le logo E/S.
Lipis sur la version obsolète d'Electron
Vous pouvez double-cliquer sur les fichiers, ce que je n'ai pas encore mentionné. Lorsque vous double-cliquez sur un fichier, l'application associée au type MIME du fichier s'ouvre généralement. Par exemple, pour .docx
, il s'agit de Microsoft Word.
Excalidraw proposait auparavant une version Electron de l'application qui acceptait ces associations de types de fichiers. Par conséquent, lorsque vous double-cliquiez sur un fichier .excalidraw
, l'application Electron Excalidraw s'ouvrait. Lipis, que vous avez déjà rencontré, était à la fois le créateur et le développeur de la version Electron d'Excalidraw. Je lui ai demandé pourquoi il pensait qu'il était possible de supprimer la version Electron:
Les utilisateurs demandent une application Electron depuis le début, principalement parce qu'ils souhaitent ouvrir des fichiers en double-cliquant dessus. Nous avions également l'intention de la proposer sur les plates-formes de téléchargement d'applications. En parallèle, quelqu'un a suggéré de créer plutôt une PWA. Nous avons donc fait les deux. Heureusement, nous avons découvert les API du projet Fugu, telles que l'accès au système de fichiers, l'accès au presse-papiers, la gestion des fichiers, etc. D'un simple clic, vous pouvez installer l'application sur votre ordinateur ou votre appareil mobile, sans le poids supplémentaire d'Electron. Il a été facile de supprimer la version Electron, de se concentrer uniquement sur l'application Web et de la transformer en PWA optimale. De plus, nous pouvons désormais publier des PWA sur le Play Store et le Microsoft Store. C'est énorme !
On pourrait dire qu'Excalidraw pour Electron n'a pas été abandonné parce qu'Electron est mauvais, pas du tout, mais parce que le Web est devenu suffisamment bon. J'aime ça !
Gestion des fichiers
Lorsque je dis que le Web est devenu "assez bon", c'est grâce à des fonctionnalités comme la gestion de fichiers à venir.
Il s'agit d'une installation standard de macOS Big Sur. Voyons maintenant ce qui se passe lorsque j'effectue un clic droit sur un fichier Exacalidraw. Je peux choisir de l'ouvrir avec Excalidraw, la PWA installée. Bien sûr, un double-clic fonctionne également, mais il est moins spectaculaire à démontrer dans une capture d'écran.
Comment cela fonctionne-t-il ? La première étape consiste à indiquer au système d'exploitation les types de fichiers que mon application peut gérer. Je le fais dans un nouveau champ appelé file_handlers
dans le fichier manifeste de l'application Web. Sa valeur est un tableau d'objets avec une action et une propriété accept
. L'action détermine le chemin d'URL à partir duquel le système d'exploitation lance votre application, et l'objet "accept" est constitué de paires clé-valeur de types MIME et des extensions de fichier associées.
{
"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"]
}
}
]
}
L'étape suivante consiste à gérer le fichier lorsque l'application est lancée. Cela se produit dans l'interface launchQueue
, où je dois définir un consommateur en appelant setConsumer()
. Le paramètre de cette fonction est une fonction asynchrone qui reçoit launchParams
. Cet objet launchParams
possède un champ appelé "files" qui me fournit un tableau de poignées de fichiers à utiliser. Je ne m'intéresse qu'au premier et à partir de ce gestionnaire de fichiers, je reçois un blob que je transmets ensuite à notre vieil ami 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 });
});
});
}
Encore une fois, si vous avez trouvé que cela allait trop vite, vous pouvez en savoir plus sur l'API File Handling dans mon article. Vous pouvez activer la gestion des fichiers en définissant l'indicateur de fonctionnalités expérimentales de la plate-forme Web. Elle devrait être disponible dans Chrome dans le courant de l'année.
Intégration du presse-papiers
Excalidraw propose également une fonctionnalité intéressante : l'intégration du presse-papiers. Je peux copier l'intégralité de mon dessin ou seulement certaines parties dans le presse-papiers, ajouter un filigrane si je le souhaite, puis le coller dans une autre application. Il s'agit d'une version Web de l'application Paint de Windows 95.
Son fonctionnement est étonnamment simple. Tout ce dont j'ai besoin est le canevas sous forme de blob, que j'écris ensuite dans le presse-papiers en transmettant un tableau à un élément avec un ClipboardItem
avec le blob à la fonction navigator.clipboard.write()
. Pour en savoir plus sur ce que vous pouvez faire avec l'API du presse-papiers, consultez l'article de Jason et le mien.
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);
}
});
};
Collaboration avec d'autres personnes
Partager une URL de session
Saviez-vous qu'Excalidraw propose également un mode collaboratif ? Différentes personnes peuvent travailler ensemble sur le même document. Pour démarrer une nouvelle session, je clique sur le bouton de collaboration en direct, puis je démarre une session. Je peux facilement partager l'URL de la session avec mes collaborateurs grâce à l'API Web Share intégrée à Excalidraw.
Collaboration en direct
J'ai simulé une session de collaboration en local en travaillant sur le logo Google I/O sur mon Pixelbook, mon téléphone Pixel 3a et mon iPad Pro. Vous pouvez voir que les modifications que j'apporte sur un appareil se répercutent sur tous les autres.
Je peux même voir tous les curseurs se déplacer. Le curseur du Pixelbook se déplace de manière régulière, car il est contrôlé par un pavé tactile, mais le curseur du téléphone Pixel 3a et celui de la tablette iPad Pro sautent, car je contrôle ces appareils en appuyant avec mon doigt.
Afficher les états des collaborateurs
Pour améliorer l'expérience de collaboration en temps réel, un système de détection d'inactivité est même en cours d'exécution. Le curseur de l'iPad Pro affiche un point vert lorsque je l'utilise. Le point devient noir lorsque je passe à un autre onglet ou application de navigateur. Et lorsque je suis dans l'application Excalidraw, mais que je ne fais rien, le curseur indique que je suis inactif, symbolisé par les trois zZZ.
Les lecteurs assidus de nos publications pourraient être enclins à penser que la détection d'inactivité est réalisée via l'API Idle Detection, une proposition à un stade précoce sur laquelle nous avons travaillé dans le cadre du projet Fugu. Attention spoiler: ce n'est pas le cas. Bien que nous ayons implémenté cette API dans Excalidraw, nous avons finalement opté pour une approche plus traditionnelle basée sur la mesure du mouvement du pointeur et de la visibilité de la page.
Nous avons envoyé des commentaires sur les raisons pour lesquelles l'API de détection d'inactivité ne résolvait pas le cas d'utilisation que nous avions. Toutes les API du projet Fugu sont développées en open source, afin que chacun puisse s'exprimer.
Lipis sur les freins à Excalidraw
À ce propos, j'ai posé à lipis une dernière question sur ce qui lui manque dans la plate-forme Web qui freine Excalidraw:
L'API File System Access est géniale, mais saviez-vous que ? La plupart des fichiers qui me tiennent à cœur se trouvent dans Dropbox ou Google Drive, et non sur mon disque dur. J'aimerais que l'API File System Access inclue une couche d'abstraction pour les fournisseurs de systèmes de fichiers distants tels que Dropbox ou Google, avec lesquels les développeurs pourraient coder. Les utilisateurs peuvent alors se détendre en sachant que leurs fichiers sont en sécurité auprès du fournisseur de services cloud de leur choix.
Je suis tout à fait d'accord avec lipis. Je vis aussi dans le cloud. J'espère que cette fonctionnalité sera bientôt implémentée.
Mode d'application à onglets
Impressionnant ! Nous avons vu de nombreuses intégrations d'API très intéressantes dans Excalidraw. Système de fichiers, gestion des fichiers, Bloc-notes, partage Web et cible de partage Web. Mais j'ai encore une chose à vous dire. Jusqu'à présent, je ne pouvais modifier qu'un seul document à la fois. Plus vraiment. Pour la première fois, profitez d'une version préliminaire du mode application à onglets dans Excalidraw. Voici à quoi cela ressemble.
Un fichier existant est ouvert dans la PWA Excalidraw installée, qui s'exécute en mode autonome. Je vais maintenant ouvrir un nouvel onglet dans la fenêtre autonome. Il ne s'agit pas d'un onglet de navigateur standard, mais d'un onglet PWA. Dans ce nouvel onglet, je peux ensuite ouvrir un fichier secondaire et travailler dessus indépendamment de la même fenêtre d'application.
Le mode d'application à onglets est encore à ses débuts, et tout n'est pas encore figé. Si vous êtes intéressé, consultez cet article pour connaître l'état actuel de cette fonctionnalité.
Conclusion
Pour vous tenir informé de cette fonctionnalité et d'autres, consultez notre outil de suivi de l'API Fugu. Nous sommes ravis de faire progresser le Web et de vous permettre d'en faire plus sur la plate-forme. À Excalidraw, qui ne cesse de s'améliorer, et à toutes les applications incroyables que vous allez créer. Commencez à créer sur excalidraw.com.
J'ai hâte de voir certaines des API que j'ai présentées aujourd'hui apparaître dans vos applications. Je m'appelle Tom. Vous pouvez me trouver sur Twitter et sur Internet en général sous le nom @tomayac. Merci beaucoup d'avoir regardé cette vidéo. Bonne fin de Google I/O.