Excalidraw e Fugu: migliorare i percorsi dell'utente principali

Qualsiasi tecnologia sufficientemente avanzata è indistinguibile dalla magia. A meno che tu non la capisca. Mi chiamo Thomas Steiner e lavoro nel team Developer Relations presso Google e in questo articolo della mia conferenza Google I/O analizzerò 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, ingegnere informatico di Facebook, ha twittato a proposito di una piccola app di disegno che aveva su cui abbiamo iniziato a lavorare. Con questo strumento puoi disegnare caselle e frecce da cartoni animati disegnate a mano. Il giorno successivo, puoi anche disegnare ellissi e testo, nonché selezionare oggetti e spostare intorno a te. Il 3 gennaio l'app aveva preso il nome Excalidraw e, come tutte le altre cose buone l'acquisto del nome di dominio è stato uno dei primi atti di Christopher. Di ora puoi utilizzare i colori ed esportare l'intero disegno come PNG.

Screenshot dell'applicazione prototipo Excalidraw che mostra il supporto di rettangoli, frecce, ellissi e testo.

Il 15 gennaio, Christopher ha pubblicato una post del blog che ha attirato molta attenzione su Twitter, inclusa la mia. Il post è iniziato 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 la cosa che davvero il mio interesse era più in basso nel post. Christopher ha scritto di aver provato qualcosa di nuovo momento: assegnare a tutti coloro che hanno ricevuto una richiesta di pull un accesso con commit incondizionato. Lo stesso giorno leggendo il post del blog, ho ricevuto una richiesta di pull che aggiungeva il supporto dell'API File System Access a Excalidraw, correggendo richiesta di funzionalità presentata da un altro utente.

Screenshot del tweet in cui annuncio il mio PR.

La mia richiesta di pull è stata unita il giorno dopo e da quel momento ho ottenuto l'accesso al commit completo. Inutile dire che Non ho abusato del mio potere. E nessun altro utente dai 149 collaboratori finora.

Oggi Excalidraw è un'app web progressiva installabile a tutti gli effetti il supporto offline, una fantastica modalità Buio e sì, la possibilità di aprire e salvare file grazie API File System Access.

Screenshot della PWA Excalidraw nello stato attuale.

Lipis spiega perché dedica così tanto tempo a Excalidraw

Siamo giunti alla fine della serie "Come sono venuta a Excalidraw" ma prima di approfondire alcuni di questi le incredibili caratteristiche di Excalidraw, ho il piacere di presentare Panayiotis. Panayiotis Lipiridis, on semplicemente noto come lipis, è il contributo più prolifico alla Excalidraw. Ho chiesto a Lipis che cosa lo spinge a dedicare così tanto tempo a Excalidraw:

Come tutte le altre persone che ho scoperto su questo progetto dal tweet di Christopher. Il mio primo contributo è stata aggiungere la libreria Apri la raccolta colori, i colori che sono parte di Excalidraw oggi stesso. Con la crescita del progetto e con molte richieste, la mia prossima grande contributo è stato quello di creare un backend per l'archiviazione dei disegni in modo che gli utenti potessero condividerli. Ma la cosa che mi spinge a contribuire è che chiunque abbia provato Excalidraw sta cercando scuse da usare di nuovo.

Sono pienamente d'accordo con le labbra. Chiunque abbia provato Excalidraw sta cercando scuse per usarlo di nuovo.

Excalidraw in azione

Ora voglio mostrarti come puoi usare Excalidraw nella pratica. Non sono un grande artista, ma Il logo di Google I/O è abbastanza semplice, quindi farò una prova. Un rettangolo è la "i", una linea può essere barra e "o" è un cerchio. Tengo premuto Maiusc, quindi ottengo un cerchio perfetto. Fammi spostare un po' lo slash, in modo che abbia un aspetto migliore. Un po' di colore per la "i" e la "o". Il blu è un buon segno. Forse uno stile di riempimento diverso? Tutto pieno o con tratteggi incrociati? No, hachure sembra fantastico. Non è perfetto, ma Questa è l'idea di Excalidraw, quindi salvo.

Faccio clic sull'icona Salva e inserisco un nome per il file nella finestra di dialogo per il salvataggio del file. In Chrome, un browser che supporta l'API File System Access, non si tratta di un download, ma di una vera operazione di salvataggio, in cui posso scegliere la posizione e il nome del file e dove, se apporto modifiche, posso semplicemente salvarlo nel lo stesso file.

Modifico il logo e imposto la "i" rosso. Se ora faccio di nuovo clic su Salva, la modifica viene salvata lo stesso file di prima. Come prova, permettimi di cancellare il canvas e riaprire il file. Come puoi vedere, il logo rosso-blu modificato è di nuovo lì.

Lavorare con i file

Sui browser che attualmente non supportano l'API File System Access, ogni operazione di salvataggio è una scaricare. In questo modo, quando apporto modifiche, ci sono più file con un numero incrementale che riempia la mia cartella Download. Nonostante questo svantaggio, posso comunque salvare il file.

Apertura dei file

Qual è il segreto? Come funziona l'apertura e il salvataggio su browser diversi che potrebbero o meno supportano 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 denominata 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 nelle Excalidraw. Questa libreria fornisce l'accesso al file system tramite API File System Access con un fallback precedente, quindi può essere utilizzata in del 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 funzione showOpenFilePicker(). Questa funzione restituisce un array di file o un singolo file, a seconda sulla selezione di più file. A questo punto rimane solo l'handle del 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 le estensioni e i tipi MIME accettati, il passaggio successivo consiste nel fare clic sull'input in modo programmatico per mostrare la finestra di dialogo Apri 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...

Passiamo al salvataggio. In Excalidraw, il salvataggio avviene in una funzione chiamata saveAsJSON(). Per prima cosa serializza l'array di elementi Excalidraw in JSON, converte il JSON in un blob e chiama un funzione chiamata fileSave(). Anche questa funzione è fornita dal 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 };
};

Esaminiamo di nuovo l'implementazione per i browser che supportano l'API File System Access. La le prime due righe sembrano un po' complesse, ma tutto ciò che fanno è negoziare i tipi MIME e estensioni. Se ho già eseguito il salvataggio e ho già un handle di file, non deve essere visualizzata alcuna finestra di dialogo per il salvataggio come mostrato nell'immagine. Se invece si tratta del primo salvataggio, viene visualizzata una finestra di dialogo del file e l'app riceve un handle per un uso futuro. Il resto è quindi la semplice scrittura sul file, che avviene mediante una streaming 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;
};

L'opzione "Salva con nome" funzionalità

Se decido di ignorare un handle di file già esistente, posso implementare un comando "Salva con nome" funzionalità per creare un nuovo file in base a uno esistente. Per farlo, apro un file esistente e ingrandisco la modifica e poi non sovrascrivere il file esistente, ma crea un nuovo file utilizzando il comando funzionalità. Il file originale rimane quindi intatto.

L'implementazione per i browser che non supportano l'API File System Access è breve, dato che crea un elemento anchor con un attributo download il cui valore è il nome file desiderato e un URL del 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();
};

Successivamente, gli utenti fanno clic sull'elemento anchor in modo programmatico. Per evitare fughe di memoria, l'URL del blob deve potrebbe essere revocato dopo l'uso. Poiché si tratta solo di un download, non verrà mai visualizzata alcuna finestra di dialogo per il salvataggio dei file vengono inseriti nella cartella predefinita Downloads.

Trascina

Una delle mie integrazioni di sistema preferite su computer è il trascinamento. In Excalidraw, quando seleziono .excalidraw nell'applicazione, si apre subito e posso iniziare ad apportare modifiche. Sui browser che supportano l'API File System Access, posso persino salvare immediatamente le modifiche. Non c'è bisogno di andare tramite una finestra di dialogo per il salvataggio dei file poiché l'handle del file richiesto è stato ottenuto tramite trascinamento operativa.

Il segreto per farlo è chiamare getAsFileSystemHandle() sul data transfer quando è supportata l'API File System Access. Quindi passo questo l'handle del file in loadFromBlob(), come ricorderai da un paio di paragrafi precedenti. Così tanti cose che puoi fare con i file: apertura, salvataggio, salvataggio eccessivo, trascinamento e rilascio. Il mio collega Pietro e ho documentato tutti questi e altri suggerimenti nel nostro articolo per recuperarli nel caso in cui tutto questo sia andato un po' troppo velocemente.

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 });
  });
}

Condivisione di file

Un'altra integrazione di sistema attualmente su Android, ChromeOS e Windows è tramite API Web Share Target. Eccomi nell'app Files nella mia cartella Downloads. IO puoi vedere due file, uno dei quali con nome non descrittivo untitled e con timestamp. Per verificare cosa contiene, faccio clic sui tre puntini, quindi condivido. Una delle opzioni che compaiono è Excalidraw. Quando tocco l'icona, vedo di nuovo che il file contiene di nuovo il logo di I/O.

Lipis nella versione Electron deprecata

Con i file di cui non ho ancora parlato puoi eseguire un doubleclick. Tipica succede quando esegui il clic su un file che l'app è associata al tipo MIME del file si apre. Ad esempio per .docx si tratta di Microsoft Word.

Excalidraw aveva una versione elettronica dell'app che supportava queste associazioni di tipi di file. Quindi quando facevi doppio clic su un file .excalidraw, la Si apriva l'app Excalidraw Electron. Lipis, che hai già incontrato in precedenza, è stata la creator e il deprecatore di Excalidraw Electron. Gli ho chiesto perché riteneva che fosse possibile ritirare il Versione elettronica:

Le persone hanno chiesto un'app Electron sin dall'inizio, principalmente perché volevano aprire i file facendo doppio clic. Inoltre, volevamo inserire l'app negli store. In parallelo, qualcuno ha suggerito di creare una PWA, quindi abbiamo fatto entrambe le cose. Per fortuna abbiamo presentato Project Fugu API come l'accesso al file system, l'accesso agli appunti, la gestione di file e altro ancora. Con un solo clic puoi installa l'app sul tuo computer o dispositivo mobile, senza il peso aggiuntivo di Electron. È stato un processo la decisione di ritirare la versione di Electron, di concentrarsi solo sull'app web e di renderla la migliore PWA possibile. In alto, ora siamo in grado di pubblicare PWA sul Play Store e sulla Negozio! È enorme!

Si potrebbe dire che Excalidraw for Electron non è deprecato perché Electron non è affatto cattivo, ma perché il web è diventato sufficiente. Mi piace!

Gestione di file

Quando dico "il web è diventato abbastanza valido", è per via di funzionalità come il file in arrivo Utilizzo.

Questa è una normale installazione di macOS Big Sur. Ora controlla cosa succede quando faccio clic con il tasto destro del mouse Excalidraw. Posso scegliere di aprirla con Excalidraw, la PWA installata. Naturalmente, fare doppio clic potrebbe funzionare anche perché è meno esagerato da dimostrare in uno screencast.

Come funziona? Il primo passaggio consiste nel rendere noti i tipi di file che la mia applicazione può gestire tipo e quantità di spazio di archiviazione necessari e sistema operativo. Faccio questa operazione in un nuovo campo denominato file_handlers nel file manifest dell'app web. È è un array di oggetti con un'azione e una proprietà accept. L'azione determina l'URL percorso in cui il sistema operativo avvia l'app e l'oggetto di accettazione sono coppie chiave-valore di MIME e le relative estensioni di file.

{
  "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 nella gestione del file all'avvio dell'applicazione. Questo accade in launchQueue in cui devo impostare un consumer chiamando, beh, setConsumer(). Il parametro a questo è 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 del 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 });
      });
    });
}

Anche in questo caso, se la velocità è troppo elevata, puoi trovare ulteriori informazioni sull'API File handling in il mio articolo. Puoi attivare la gestione di file impostando la piattaforma web sperimentale il flag delle caratteristiche. È programmato per essere disponibile su Chrome entro la fine dell'anno.

Integrazione degli appunti

Un'altra interessante funzionalità di Excalidraw è l'integrazione negli appunti. posso copiare l'intero disegno solo alcune parti negli appunti, magari aggiungendo una filigrana, se lo voglio, e incollandola in un'altra app. A proposito, questa è una versione web dell'app Windows 95 Paint.

Il funzionamento è sorprendentemente semplice. Tutto ciò che mi serve è la tela sotto forma di blob, che poi scrivo negli appunti passando un array di un solo elemento con ClipboardItem con il blob alla Funzione navigator.clipboard.write(). Per ulteriori informazioni su cosa puoi fare con gli appunti API, consulta il mio articolo di Giuseppe.

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à collaborativa? Persone diverse possono collaborare lo stesso documento. Per avviare una nuova sessione, faccio clic sul pulsante Collaborazione dal vivo e avvio una durante la sessione. Posso condividere facilmente l'URL della sessione con i miei collaboratori grazie alla L'API Web Share integrata da Excalidraw.

Collaborazione dal vivo

Ho simulato una sessione di collaborazione in locale lavorando sul logo Google I/O sul mio Pixelbook. il mio smartphone Pixel 3a e il mio iPad Pro. Le modifiche che apporto su un dispositivo vengono applicate tutti gli altri dispositivi.

Posso persino vedere tutti i cursori muoversi. Il cursore del Pixelbook si sposta in modo costante, dato che è controllato da un trackpad, ma il cursore del telefono Pixel 3a e quello del tablet dell'iPad Pro saltano da una parte all'altra, dato che controlla questi dispositivi toccando con il dito.

Visualizzare gli stati dei collaboratori

Per migliorare l'esperienza di collaborazione in tempo reale, è in esecuzione anche un sistema di rilevamento di inattività. Il cursore dell'iPad Pro mostra un puntino verde quando lo utilizzo. Il pallino diventa nero quando passo a una in un'altra scheda o app del browser. E quando sono nell'app Excalidraw ma non faccio nulla, il cursore mi riporta come inattivo, simboleggiato dalle tre zZZ.

I lettori accaniti delle nostre pubblicazioni potrebbero essere inclini a pensare che il rilevamento dell'inattività venga realizzato attraverso l'API Idle Detection, una proposta in fase iniziale su cui abbiamo lavorato nel contesto di Project Fugu. Spoiler: non è così. L'implementazione basata su questa API in Excalidraw, alla fine, abbiamo deciso di adottare un approccio più tradizionale basato il movimento del puntatore e la visibilità della pagina.

Screenshot del feedback relativo al rilevamento di inattività registrato nel repository WICG Idle Detection.

Abbiamo inviato un feedback sul motivo per cui l'API Idle Detection non era quello di risolvere il nostro caso d'uso. Tutte le API di Project Fugu sono sviluppate in forma aperta, quindi tutti possono intervenire e far sentire la propria voce.

Lipis su ciò che frena Excalidraw

A proposito, ho fatto a lipis un'ultima domanda su cosa pensa che manchi sul web che trattiene Excalidraw:

L'API File System Access è eccezionale, ma sai una cosa? La maggior parte dei file che mi interessano in questi giorni nella mia Dropbox o Google Drive, non sul mio disco rigido. Vorrei che l'API File System Access includi un livello di astrazione per consentire ai fornitori di file system remoti come Dropbox o Google di integrare con cui gli sviluppatori potevano scrivere il codice. In questo modo gli utenti possono rilassarsi e sapere che i loro file sono al sicuro con il cloud provider di cui si fidano.

Sono pienamente d'accordo con le labbra, anche io vivo nel cloud. Speriamo che venga implementata a breve.

Modalità di applicazione a schede

Wow! Abbiamo visto molte integrazioni API davvero eccezionali in Excalidraw. File system, gestione file, appunti, condivisione web e target condivisione web. Ma ecco un'altra cosa. Finora, potevo sempre solo modificare un documento alla volta. Ora non più. Per la prima volta, è disponibile una versione in anteprima di con la modalità di applicazione a schede in Excalidraw. Ecco come appare.

Ho già un file aperto nella PWA Excalidraw installata in esecuzione in modalità autonoma. Adesso Apro una nuova scheda nella finestra autonoma. Non si tratta di una normale scheda del browser, ma di una scheda PWA. In questo nuova scheda Posso quindi aprire un file secondario e lavorarci indipendentemente dalla stessa finestra dell'app.

La modalità di applicazione a schede è ancora in fase di sviluppo e non è tutto definitivo. Se ti interessa, continua a leggere per conoscere lo stato attuale di questa funzione il mio articolo.

Chiusura

Per rimanere al passo con questa e altre funzionalità, assicurati di guardare il nostro Tracker dell'API Fugu. Siamo entusiasti di portare avanti il web e ti consentono di fare di più sulla piattaforma. Con ExcaliDraw in continuo miglioramento, ed ecco a tutti i fantastiche applicazioni che creerai. Inizia a creare da excalidraw.com.

Non vedo l'ora di vedere alcune delle API che ho mostrato oggi appariranno nelle tue app. Mi chiamo Tom puoi trovarmi come @tomayac su Twitter e su internet in generale. Grazie per l'attenzione e buon lavoro con Google I/O.