任何先進的技術都與魔法無異。除非你瞭解,我是 Google 開發人員關係團隊的 Thomas Steiner。在 Google I/O 大會的演講中,我將介紹一些新的 Fugu API,並說明這些 API 如何改善 Excalidraw PWA 中核心使用者歷程,因此你可以從這些構想中汲取靈感,並將這些靈感應用到自己的應用程式。
我如何開始使用 Excalidraw
我想先說一個故事Facebook 軟體工程師 Christopher Chedeau 在 2020 年 1 月 1 日在推文針對某個小型繪圖應用程式進行了這篇推文。使用這項工具,您可以繪製看起來像卡通和手繪的方塊和箭頭。第二天還可以繪製刪節號和文字,也可選取物件並四處移動。應用程式已於 1 月 3 日命名了 Excalidraw,而且就像每個好面的專案一樣,購買網域名稱是 Christopher 最先從事的策略之一。您現在可以使用顏色,並將整個繪圖匯出為 PNG。
1 月 15 日,Christopher 發布了一篇網誌文章,在 Twitter 和我的網站上獲得了大量關注。貼文寫著一些令人印象深刻的數據:
- 12K 位活躍使用者
- GitHub 上有 1,500 顆星
- 26 位貢獻者
如果專案成立的時間只有兩週前,成效還不錯,但真正引起我興趣的是文章後半段的內容。Christopher 寫道,這次他嘗試了新做法:為所有提交合併要求的使用者提供無條件提交存取權。在閱讀網誌文章當天,我提出提取要求,將 File System Access API 支援功能新增至 Excalidraw,修正了某人提出的功能要求。
我的提取要求在一天後合併,後來就擁有完整的修訂版本存取權。不用說,我並沒有濫用權力。到目前為止,149 位貢獻者也沒有其他貢獻者
Excalidraw 是開發成全面的漸進式網頁應用程式,不僅支援離線支援功能,還提供令人驚豔的深色模式,可以藉助 File System Access API 開啟及儲存檔案。
Lipis 談到為何他會花費大量時間使用 Excalidraw
這就是「如何開啟 Excalidraw」故事的最終點 但在深入探索 Excalidraw 強大的功能之前 我很高興能介紹 PanayiotisPanayiotis Lipiridis 會在網際網路上簡稱為 lipis,以貢獻最多的是 Excalidraw。我問了 lipis 為何願意花這麼多時間投入 Excalidraw:
就像其他人在 Christopher 的推文中瞭解這項專案,我第一個貢獻是新增 Open Color 資料庫,這些顏色目前仍是 Excalidraw 的一部分。隨著專案規模擴大,我們收到許多要求,因此我接下來的重要貢獻是建構用於儲存繪圖的後端,方便使用者分享繪圖。但真正讓我貢獻的意義在於,曾經嘗試過 Excalidraw 的人會想找出再次使用它的藉口。
我完全同意嘴唇。有些人嘗試過 Excalidraw,他們會想找再度使用的原因。
Excalidraw 實際使用教學
我現在要示範在實務中如何應用 Excalidraw。我不太喜歡這裡的藝術家 但 Google I/O 標誌已經夠簡單了,讓我來試試看方塊是「i」,線條可以是斜線,「o」則是圓形。我按住 shift 鍵,以便繪製完美的圓形。我把斜線稍微拉一點 讓它看起來更美觀現在,「i」和「o」會顯示一些顏色。藍色代表做得很好。也許是採用不同的填滿樣式?全盤測試,還是越過成功?不,虛線圖看起來很棒。這不是完美的結果,但這是 Excalidraw 的構想 所以我儲存吧
我按一下儲存圖示,並在檔案儲存對話方塊中輸入檔案名稱。在支援 File System Access API 的瀏覽器 Chrome 中,這並非下載作業,而是真正的儲存作業,我可以選擇檔案的位置和名稱,如果我進行編輯,可以直接將其儲存到同一個檔案中。
我來變更標誌,將「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 小型程式庫,我們在 Exaclidraw 中使用這個程式庫。這個程式庫透過舊版的備用方案透過 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
屬性值,其值是所需的檔案名稱,另一個是 blob 網址做為 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();
};
之後,系統會透過程式輔助方式點擊錨定元素。為避免記憶體流失,使用後必須撤銷 Blob 網址。由於這只是下載作業,因此不會顯示任何檔案儲存對話方塊,所有檔案都會儲存在預設的 Downloads
資料夾中。
拖曳
我最喜歡的電腦版系統整合是拖曳。在 Excalidraw 中,將 .excalidraw
檔案拖放至應用程式時,就會立即開啟,我可以開始編輯。在支援 File System Access API 的瀏覽器上,我甚至可以立即儲存變更。您不需要使用檔案儲存對話方塊,因為系統已透過拖曳作業取得必要的檔案控制代碼。
做法是在系統支援 File System Access API 時,對資料移轉項目呼叫 getAsFileSystemHandle()
。接著,我會將這個檔案句柄傳遞至 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
資料夾中的「檔案」應用程式中。我看到兩個檔案,其中一個是非描述元名稱 untitled
和時間戳記。如要查看其中內容,我點選三點圖示,然後分享
畫面上出現的其中一個選項就是「Excalidraw」。輕觸圖示後,我們就能再次看到檔案只包含 I/O 標誌。
已淘汰的 Electron 版本 Lipis
您可以雙擊檔案,這是我尚未提及的操作方式。產生檔案時,通常會發生的情況是,與檔案的 MIME 類型相關聯的應用程式會開啟。以 .docx
為例,這應是 Microsoft Word。
Excalidraw 曾經有支援這類檔案類型關聯的 Electron 版本,因此當您雙擊 .excalidraw
檔案時,Excalidraw Electron 應用程式就會開啟。您先前曾見過的 Lipis 是 Excalidraw Electron 的創作者和淘汰者。我問他為什麼覺得可以淘汰 Electron 版本:
自創立以來,使用者就不斷要求使用 Electron 應用程式,主要是因為想按兩下滑鼠即可開啟檔案。我們也打算將應用程式上架至應用程式商店。與此同時,有人建議改為建立 PWA,因此我們同時執行這兩項作業。幸運的是,我們認識了 Project Fugu API,例如檔案系統存取權、剪貼簿存取權、檔案處理等。輕輕一按,即可在電腦或行動裝置上安裝應用程式,不必額外花費 Electron。我們很容易決定淘汰 Electron 版本,只集中在網頁應用程式,要將其打造為可能的最佳 PWA。最重要的是,我們現在可以將 PWA 發布至 Play 商店和 Microsoft 商店!太棒了!
有人可能會說 Electron 的 Excalidraw 沒有淘汰,因為 Electron 並非壞事,而是網路已發展得夠好。我喜歡這個!
檔案處理
我說「網頁已經足夠好」,是因為有即將推出的檔案處理功能等功能。
這是一般 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
物件含有稱為「檔案」的欄位,可讓我取得要使用的檔案控制代碼。我只在意第一個,並從這個檔案句柄取得 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 版 Paint 應用程式的網路版本。
這個過程非常簡單。我只需要將畫布做為 Blob,然後透過傳遞具有 Blob 的 ClipboardItem
的單一元素陣列,將 Blob 寫入剪貼簿,並傳送至 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、Pixel 3a 手機和 iPad Pro 上繪製 Google I/O 標誌,模擬在本機進行協作。你可以看到我在一台裝置上所做的變更,會反映在所有其他裝置上。
我甚至可以看到所有游標移動。Pixelbook 的游標是透過觸控板控制,因此移動得很穩定,但 Pixel 3a 手機的游標和 iPad Pro 平板電腦的游標會跳動,因為我用手指輕觸來控制這些裝置。
查看協作者狀態
為了改善即時協作體驗,即使系統正在執行閒置偵測系統,我使用 iPad Pro 時,遊標的遊標會顯示綠點。當我切換至其他瀏覽器分頁或應用程式時,這個點會變成黑色。當我在 Excalidraw 應用程式中,但什麼事都沒做時,游標會顯示為閒置狀態,以三個 zZZ 做為象徵。
熱心閱讀我們出版品的讀者可能會認為,閒置偵測功能是透過 Idle Detection API 實現的,這是在 Project Fugu 的背景下提出的早期提案。小心有雷雖然我們在 Excalidraw 中實作了這個 API,但最後我們決定採用更傳統的方法,以測量指標移動和頁面顯示情況為基礎。
我們提出了意見回饋,說明 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 大會的其他內容。