《Excalidraw》與《Fugu》:改善核心使用者歷程

任何先進的技術都與魔法無異。除非你明白。我是 Google 開發人員關係團隊的 Thomas Steiner。在 Google I/O 大會的演講中,我將介紹一些新的 Fugu API,並說明這些 API 如何改善 Excalidraw PWA 的核心使用者歷程,因此你可以從這些構想中汲取靈感,並將這些靈感應用到自己的應用程式。

如何前往 Excalidraw

我想先說一個故事自 2020 年 1 月 1 日起 Facebook 軟體工程師 Christopher Chedeau 在 Twitter 推文,他曾使用一款小型繪圖應用程式 你可以使用這項工具畫出方塊和箭頭 。查看次日還能繪製刪節號和文字,選取物件並移動 幫助他們快速瞭解這款應用程式在 1 月 3 日更名為 Excalidraw 命名了,就像每個好面一樣 時,購買網域名稱是 Christopher 的第一波行動之一。變更者: 即可使用顏色將整個繪圖匯出為 PNG 檔案。

Excalidraw 原型應用程式的螢幕截圖,顯示支援的矩形、箭頭、刪節號和文字。

1 月 15 日,Christopher 公布 將網誌文章 受到很多關注,包括 mine。貼文寫著一些令人印象深刻的數據:

  • 1.2 萬個不重複活躍使用者
  • GitHub 上有 1,500 顆星
  • 26 位貢獻者

如果專案成立的時間只有兩週前,成效還不錯,但更值得一提的是 讓他們對貼文的興趣下滑Christopher 寫下許多嘗試新事物 時間:將提取要求無條件的修訂版本提供給所有人。同一天 閱讀網誌文章,我收到提取請求 使 Excalidraw 支援 File System Access API 支援 已索取的事件功能要求

宣布公關宣傳的推文螢幕截圖。

我的提取要求在一天後合併,後來就擁有完整的修訂版本存取權。不用說 我並沒有濫用力量。到目前為止,149 位貢獻者也沒有其他貢獻者

Excalidraw 是新型的可安裝式漸進式網頁應用程式, 離線支援、令人驚豔的深色模式 自動開啟及儲存檔案 File System Access API。

Excalidraw PWA 目前狀態的螢幕截圖。

Lipis 瞭解他為何投入太多時間去 Excalidraw

這就是 我來到 Excalidraw 車款的結尾了但在開始深入介紹 Excalidraw 具備的超棒功能,我很高興能介紹 Panayiotis。潘亞瑞歐提斯里皮里迪斯 (Panayiotis Lipiridis) 網際網路簡稱為 lipis,是網路貢獻者 Excalidraw。我詢問了唱謊的動機,是他投入大量時間去 Excalidraw 的動機:

就像其他人在 Christopher 的推文中瞭解這項專案,我的首次貢獻 是新增 Open Color Library,超過 這也是 Excalidraw 身體的一部分隨著這項專案不斷擴增,收到許多要求後 這個平台的重點在於打造一個儲存繪圖的後端,方便使用者與大家分享。但 我真的能鼓勵我貢獻一己之力 再次提醒。

我完全同意嘴唇。有些人嘗試過 Excalidraw,他們會想找再度使用的原因。

Excalidraw 實際使用教學

我現在要示範在實務中如何應用 Excalidraw。我不擅長這個藝術家 Google I/O 標誌非常簡單,讓我來試試看。方塊是「i」,線條可以是 斜線,「o」是一個圓形。接著按住 Shift 鍵,這樣我得到完美圓圈了。讓我動起來 看起來更好現在「i」的新顏色和「o」鍵藍色是很好的。不確定 不同的填滿樣式?全盤測試,還是越過成功?啊,哈哈,看起來很棒。非常完美 這就是 Excalidraw 開發的想法 所以我儲存吧

我按一下儲存圖示,並在檔案儲存對話方塊中輸入檔案名稱。在 Chrome 中 支援 File System Access API,這並不是下載作業,而是真實的儲存作業,其中 選擇檔案位置和名稱 如果我編輯過檔案,可以直接儲存檔案 同一個檔案。

變更標誌,將「i」改為「i」紅色。如果現在再按一下 [儲存],我的修改就會儲存到 與先前的相同檔案為了安全起見,我先清理畫布,再重新開啟檔案。如畫面所示 修改過的紅藍色標誌就再次出現

使用檔案

在不支援 File System Access API 的瀏覽器中,每次儲存作業都是 因此當我進行變更時,結果出現多個檔案的遞增編號 就是填滿「下載」資料夾的檔案名稱但儘管這缺點,我還是可以儲存檔案。

開啟檔案

那到底有什麼秘訣呢?如何使用不同瀏覽器開啟及儲存 (無論瀏覽器與否) 是否支援 File System Access API?在 Excalidraw 中開啟檔案會在名為 loadFromJSON)(),而後者會呼叫名為 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);
};

fileOpen() 函式來自我編寫的小型程式庫 更新憑證中的 browser-fs-access Excalidraw。這個程式庫可透過 含有舊版備用選項的 File System Access API,以便用於任何 。

首先,我會說明 API 支援時的實作。協商 系統接受的 MIME 類型和副檔名,中央呼叫 File System Access API 的 函式 showOpenFilePicker()。這個函式會傳回檔案陣列或單一檔案 (依附關係) 檢視是否多個檔案接下來只剩下在檔案中加入檔案控制代碼 物件,讓系統能夠再次擷取。

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

備用廣告實作需要 "file" 類型的 input 元素。協議 接受系統接受的 MIME 類型和副檔名,下一步是以程式輔助方式按一下輸入 元素,以便顯示檔案開啟對話方塊。變更時,也就是使用者選取 多個檔案,承諾就會解析

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

儲存檔案

可以儲存了。在 Excalidraw 中,儲存作業會在名為 saveAsJSON() 的函式中進行。第一個 將 Excalidraw 元素陣列序列化為 JSON,然後將 JSON 轉換為 blob,然後呼叫 函式 fileSave()。這個函式同樣是由 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 };
};

再次讓我先看看支援 File System Access API 的瀏覽器的實作方式。 前幾行看起來有點複雜,但所有內容都是協商 MIME 類型和檔案 。如果我曾經儲存過檔案,且已經有檔案控制代碼,便不需要儲存對話方塊 。但如果這是第一次儲存,系統會顯示檔案對話方塊,應用程式也會取得檔案控制代碼 稍後使用接下來,剩下的工作是寫入檔案 可寫入的串流

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

將「另存新檔」精選內容

如果我決定忽略現有的檔案控制代碼,可以實作「另存新檔」特徵建立 建立新的檔案想示範一下,先開啟現有檔案,然後 修改檔案,不會覆寫現有檔案,而是使用「另存新檔」建立新檔案 而不是每個特徵的分數這不會影響原始檔案。

對於不支援 File System Access API 的瀏覽器來說,實作相當短,因為所有 是建立含有 download 屬性的錨點元素,其值是所需的檔案名稱, 做為 href 屬性值的 blob 網址。

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

之後,系統會透過程式輔助方式點擊錨定元素。為避免記憶體流失,blob 網址需 。由於這只是下載內容,系統不會顯示任何檔案儲存對話方塊,而且 檔案會存放在預設的 Downloads 資料夾中。

拖曳

我最喜歡的電腦版系統整合是拖曳。在 Excalidraw 節點中 .excalidraw 檔案匯入應用程式,就可以立即開啟應用程式,而且我就可以開始編輯。使用瀏覽器 支援 File System Access API,我甚至可以立即儲存變更。不需要 透過檔案儲存對話方塊,因為已透過拖曳方式取得所需的檔案控制代碼 作業。

實作方法是呼叫 getAsFileSystemHandle() 上的 data Transfer 項目。我接著將這張投影片 檔案控制代碼是 loadFromBlob(),您可能想記得上述幾個段落。許多 例如開啟、儲存、過度儲存、拖曳、放置。我的同事 Pete 我也在文章中記錄了所有技巧和其他內容,方便您 補上進度,以防萬一。

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

分享檔案

Android、ChromeOS 和 Windows 現有的另一項系統整合是: Web Share Target API。這是「Downloads」資料夾中的「檔案」應用程式。I 可以查看兩個檔案,其中一個檔案名稱為 untitled 的非 Descript 名稱,以及一個時間戳記。為了瞭解 然後依序點選三點圖示和「分享」 Excalidraw。再次輕觸圖示後,我們就能發現檔案只包含 I/O 標誌。

已淘汰的 Electron 版本 Lipis

如果有檔案處理我尚未討論過的檔案,請啟用該檔案。通常產生的影響 當您匯入檔案時,會發生這種問題,是因為應用程式與檔案的 MIME 類型相關聯 開啟後,以 .docx 為例,這應是 Microsoft Word。

用來執行 Electron 版本的應用程式 系統支援這類檔案類型關聯,因此當您按兩下 .excalidraw 檔案時, Excalidraw Electron 應用程式會開啟。之前認識的 Lipis 既是創作者 和 Excalidraw Electron 的解舊器我問他為什麼覺得自己能淘汰 電子版本:

我們從一開始就一直在尋求 Electron 應用程式的要求,主要是因為 按兩下即可開啟檔案我們也打算將應用程式放在應用程式商店。同時,有些人 因此我們建議改為建立 PWA,因此我們就這麼做幸好,我們已介紹 Project Fugu 檔案系統存取權、剪貼簿存取、檔案處理等 API。只要按一下滑鼠 不必額外投入 Electron 的 Android 裝置,而是在電腦或行動裝置上安裝應用程式。這太簡單了 然後集中在網路應用程式上 最理想 PWA此外,我們現在可以將 PWA 發布至 Play 商店和 Microsoft 商店!非常好!

有人可能會說 Electron 的 Excalidraw 沒有淘汰,因為 Electron 並非壞人,而是 是因為網路變得更加完善我喜歡!

檔案處理

我說「網路變得更好了」,那是 YouTube 即將推出的檔案 處理

這是 macOS Big Sur 的定期安裝。現在來看看如果我用滑鼠右鍵按一下 Excalidraw 檔案。我可以選擇使用已安裝的 PWA Excalidraw 開啟應用程式當然 連按兩下是可行的做法,如果是在螢幕側錄中示範,那樣的戲劇效果也較低。

運作方式第一步是讓應用程式能夠處理 以及作業系統方法是在網頁應用程式資訊清單的新欄位中 (file_handlers) 執行這項作業。結果 值是含有動作及 accept 屬性的物件陣列。動作會決定網址 作業系統在哪個路徑啟動應用程式;接受物件為 MIME 的鍵/值組合 類型的檔案關聯。

{
  "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"]
      }
    }
  ]
}

下一步是在應用程式啟動時處理檔案。這會在 launchQueue中進行 必須呼叫 setConsumer() 來設定用戶端 函式是接收 launchParams 的非同步函式。這個 launchParams 物件 具有稱為「file」的欄位,取得可用的檔案控制代碼陣列。我只關心 第一和這個檔案控制代碼中,我得到一個 blob,接著我傳遞給老朋友 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 });
      });
    });
}

同樣地,如果速度太快,您可以前往以下頁面進一步瞭解 File Handling API 我的文章。設定實驗性網路平台,即可啟用檔案處理功能 功能旗標預計今年稍晚才會在 Chrome 中顯示。

剪貼簿整合

Excalidraw 的另一項實用功能是剪貼簿整合功能。我可以複製整個繪圖,或是 並貼到剪貼簿中 還可以視需要加上浮水印 其他應用程式。順帶一提,現在是 Windows 95 塗料應用程式的網頁版。

這個過程非常簡單。我需要的畫布是 blob 方法是將含有 blob 的 ClipboardItem 元素陣列傳遞至剪貼簿 navigator.clipboard.write() 函式。進一步瞭解剪貼簿的用途 API,請參閱 Jason 和我的文章

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

與他人協作

分享工作階段網址

您知道 Excalidraw 也有協作模式嗎?不同的人可以一起處理 相同文件如要發起新的會議,我點選「即時協作」按鈕,然後開始 會很有幫助我可以輕鬆將工作階段網址分享給協作者, 已與 Excalidraw 整合的 Web Share API

即時協作

我曾在 Pixelbook 上編輯 Google I/O 標誌,模擬現場演練過程。 我的 Pixel 3a 手機和 iPad Pro可以看到我在某部裝置上所做的變更 和其他裝置。

我甚至可以看到所有遊標在移動上移動了Pixelbook 的遊標受到控制,因此遊標會穩定移動 這時 Pixel 3a 手機的遊標和 iPad Pro 的平板電腦遊標會在移動時跳動 控制這些裝置

查看協作者狀態

為了改善即時協作體驗,即使系統正在執行閒置偵測系統, 我使用 iPad Pro 時,遊標的遊標會顯示綠點。當我切換至黑色畫面時 瀏覽器分頁或應用程式。在 Excalidraw 應用程式中顯示時 遊標顯示我處於閒置狀態,並以三個 zZZ 符號代表。

對我們出版物的忠實讀者,可能會認為可透過以下方式實現閒置偵測 Idle Detection API,這是 Google 在 Project Fugu 的背景資訊小心有雷雖然我們是根據這個 API 進行實作 最後,我們決定採用較傳統做法, 指標移動和頁面瀏覽權限。

在 WICG 閒置偵測存放區中放置的閒置偵測意見回饋的螢幕截圖。

我們針對 Idle Detection API 提出意見回饋 無法解決我們現有的應用情況所有 Project Fugu API 都是在公開的情況下進行開發 每個人都能參與並聽見他們的心聲!

蓋在外省的埃及開特車上

關於這一點,我問了最後一個問題,請大家分享他認為網路上遺漏的內容 同時保留 Excalidraw 的平台:

File System Access API 很不錯,但你知道什麼嗎?我關心的大多數檔案 儲存在 Dropbox 或 Google 雲端硬碟,而非硬碟。我希望 File System Access API 為 Dropbox 或 Google 等遠端檔案系統供應商提供抽象層,以便整合 而開發人員也能根據這些原則找到合適的程式碼使用者隨後可以放鬆並確定自己的檔案安全無虞 與信任的雲端服務供應商之間建立連線

我完全同意 Lipis,我也住在雲端。我們希望能導入這項功能 。

分頁式應用程式模式

太棒了!Excalidraw 導入了許多絕佳的 API 整合功能, 檔案系統檔案處理剪貼簿網路分享網路共用目標。但還有一件事,但至今 一次只能編輯一份文件答案是不需要。歡迎您初次體驗 Excalidraw 中執行分頁式應用程式模式這是外觀。

如果已安裝的 Excalidraw PWA 中正在執行獨立模式,我並開啟了這個檔案。現在 我在獨立視窗中開啟新分頁。這不是一般瀏覽器分頁,而是 PWA 分頁。在本 新分頁,接著可以開啟次要檔案,並直接從同一個應用程式視窗獨立編輯這些檔案。

分頁式應用程式模式目前處於早期階段,並非全都是一成不變。如果您是 有興趣,請務必閱讀本功能在 我的文章

Closing

如要即時掌握這項功能和其他功能的最新消息,請務必觀看我們的 Fugu API 追蹤程式:我們很高興能推動網路發展, 可讓你在平台上執行更多操作我們致力改善 Excalidraw 打造出色的應用程式開始創作吧 excalidraw.com.

我等不及看到我今天在應用程式中顯示的 API 了。我是 Tom。 你在 Twitter 和網際網路上都能以 @tomayac 搜尋其他人。 非常感謝收看,也盡情享受 Google I/O 大會的其他內容。