Любая достаточно продвинутая технология неотличима от магии. Если только вы её не понимаете. Меня зовут Томас Штайнер, я работаю в отделе по работе с разработчиками в Google. В этом отрывке из моего выступления на Google I/O я рассмотрю некоторые новые API Fugu и то, как они улучшают взаимодействие основных пользователей с Excalidraw PWA, чтобы вы могли вдохновиться этими идеями и применить их в своих приложениях.
Как я пришел в Excalidraw
Хочу начать с истории. 1 января 2020 года Кристофер Шедо , инженер-программист Facebook, написал в Твиттере о небольшом приложении для рисования, над которым он начал работать. С помощью этого инструмента можно было рисовать прямоугольники и стрелки, которые выглядели мультяшными и нарисованными от руки. На следующий день появилась возможность рисовать эллипсы и текст, а также выбирать объекты и перемещать их. 3 января приложение получило название Excalidraw, и, как и любой хороший сторонний проект, одним из первых действий Кристофера стала покупка доменного имени . К тому времени можно было использовать цвета и экспортировать весь рисунок в формате PNG.
15 января Кристофер опубликовал пост в блоге , который привлёк много внимания в Твиттере, в том числе и моё. Публикация начиналась с впечатляющей статистики:
- 12 тыс. уникальных активных пользователей
- 1,5 тыс. звезд на GitHub
- 26 участников
Для проекта, начавшегося всего две недели назад, это совсем неплохо. Но то, что действительно привлекло моё внимание, было дальше в посте. Кристофер написал, что на этот раз он попробовал кое-что новое: предоставил всем, кто получил запрос на извлечение, безусловный доступ к коммиту. В тот же день, когда я читал пост в блоге, я получил запрос на извлечение , который добавил поддержку API доступа к файловой системе в Excalidraw, исправив запрос на добавление функции , который кто-то подал.
Мой запрос на включение изменений был объединён на следующий день, и с этого момента у меня был полный доступ к коммитам. Само собой, я не злоупотреблял своими полномочиями. Как и никто другой из 149 участников.
Сегодня Excalidraw — это полноценное устанавливаемое прогрессивное веб-приложение с поддержкой офлайн-режима, потрясающим темным режимом и, конечно же, возможностью открывать и сохранять файлы благодаря API доступа к файловой системе.
Липис рассказывает, почему он уделяет так много времени Excalidraw
Итак, на этом моя история о том, как я пришёл к Excalidraw, подходит к концу, но прежде чем я углублюсь в некоторые удивительные функции Excalidraw, я с удовольствием представляю Панайотиса. Панайотис Липиридис, известный в интернете просто как lipis , — самый плодовитый участник Excalidraw. Я спросил lipis, что побуждает его посвящать столько времени Excalidraw:
Как и все остальные, я узнал об этом проекте из твита Кристофера. Моим первым вкладом стало добавление библиотеки Open Color — цветов, которые до сих пор являются частью Excalidraw. По мере роста проекта и поступления множества запросов моим следующим крупным вкладом стала разработка бэкэнда для хранения рисунков, чтобы пользователи могли ими делиться. Но что действительно мотивирует меня к участию, так это то, что все, кто попробовал Excalidraw, ищут повод снова им воспользоваться.
Полностью согласен с lipis. Кто пробовал Excalidraw, тот ищет повод снова его использовать.
Excalidraw в действии
Хочу показать вам, как использовать Excalidraw на практике. Я не великий художник, но логотип Google I/O достаточно прост, так что давайте попробую. Прямоугольник — это «i», линия может быть косой чертой, а «o» — кругом. Я удерживаю Shift , чтобы получился идеальный круг. Давайте немного сдвинем косую черту, чтобы выглядело лучше. Теперь немного цвета для «i» и «o». Синий — неплохо. Может быть, другой стиль заливки? Сплошная или штриховка? Нет, штриховка выглядит отлично. Она не идеальна, но в этом и заключается идея Excalidraw, так что давайте сохраним её.
Я нажимаю на значок сохранения и ввожу имя файла в диалоговом окне сохранения. В Chrome, браузере с поддержкой API доступа к файловой системе, это не загрузка, а полноценное сохранение, где я могу выбрать местоположение и имя файла, а если я внесу изменения, просто сохранить их в тот же файл.
Давайте изменю логотип и сделаю букву «i» красной. Если я снова нажму «Сохранить», мои изменения сохранятся в том же файле, что и раньше. Для проверки очищу холст и снова открою файл. Как видите, изменённый красно-синий логотип снова на месте.
Работа с файлами
В браузерах, которые в настоящее время не поддерживают API доступа к файловой системе, каждая операция сохранения представляет собой загрузку, поэтому при внесении изменений в папку «Загрузки» попадает несколько файлов с увеличивающимся номером в имени. Но, несмотря на этот недостаток, я всё равно могу сохранить файл.
Открытие файлов
Так в чём же секрет? Как открытие и сохранение файлов работают в разных браузерах, которые могут поддерживать или не поддерживать 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. Эта библиотека обеспечивает доступ к файловой системе через API доступа к файловой системе с устаревшим резервным вариантом, поэтому её можно использовать в любом браузере.
Позвольте мне сначала показать вам реализацию, когда API поддерживается. После согласования допустимых типов MIME и расширений файлов, центральным этапом становится вызов функции showOpenFilePicker()
из API доступа к файловой системе. Эта функция возвращает массив файлов или один файл, в зависимости от того, выбрано ли несколько файлов. Остаётся только добавить дескриптор файла к объекту файла, чтобы его можно было извлечь снова.
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;
};
};
Резервная реализация основана на элементе input
типа "file"
. После согласования принимаемых 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;
};
Функция «сохранить как»
Если я решу игнорировать уже существующий дескриптор файла, я могу реализовать функцию «сохранить как» для создания нового файла на основе существующего. Чтобы продемонстрировать это, открою существующий файл, внесу некоторые изменения, а затем не перезапишу существующий файл, а создам новый с помощью функции «Сохранить как». При этом исходный файл останется нетронутым.
Реализация для браузеров, не поддерживающих API доступа к файловой системе, короткая, поскольку все, что она делает, — это создает элемент привязки с атрибутом download
, значением которого является желаемое имя файла, и URL-адресом двоичного двоичного объекта в качестве значения атрибута 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();
};
Затем происходит программный клик по якорному элементу. Во избежание утечек памяти URL-адрес BLOB-объекта необходимо отозвать после использования. Поскольку это всего лишь загрузка, диалоговое окно сохранения файла не отображается, и все файлы помещаются в папку Downloads
по умолчанию.
Перетаскивание
Одна из моих любимых системных интеграций на десктопе — это перетаскивание. В Excalidraw, когда я перетаскиваю файл .excalidraw
в приложение, он сразу же открывается, и я могу начать редактирование. В браузерах, поддерживающих API доступа к файловой системе, я могу даже сразу сохранять изменения. Не нужно открывать диалоговое окно сохранения файла, поскольку необходимый дескриптор файла уже получен в результате перетаскивания.
Секрет этого заключается в вызове метода getAsFileSystemHandle()
для элемента передачи данных , когда поддерживается API доступа к файловой системе. Затем я передаю этот дескриптор файла в метод loadFromBlob()
, который вы, возможно, помните из пары абзацев выше. С файлами можно делать множество вещей: открывать, сохранять, пересохранять, перетаскивать. Мы с коллегой Питом задокументировали все эти и другие трюки в нашей статье, так что вы можете наверстать упущенное, если всё прошло слишком быстро.
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, — это API Web Share Target . Вот я в приложении «Файлы» в папке Downloads
. Я вижу два файла, один из которых с непонятным названием untitled
и временной меткой. Чтобы проверить содержимое, я нажимаю на три точки, затем «Поделиться», и один из появившихся вариантов — Excalidraw. Нажав на значок, я вижу, что файл снова содержит только логотип I/O.
Lipis на устаревшей версии Electron
Одна из функций, о которой я ещё не рассказывал, — это двойной щелчок по файлу. Обычно при двойном щелчке по файлу открывается приложение, соответствующее MIME-типу файла. Например, для файлов формата .docx
это Microsoft Word.
Раньше у Excalidraw была версия для Electron , которая поддерживала такие ассоциации типов файлов, поэтому при двойном щелчке по файлу .excalidraw
открывалось приложение Excalidraw Electron. Липис, с которым вы уже встречались, был одновременно создателем и тем, кто объявил Excalidraw Electron устаревшим. Я спросил его, почему он считает возможным прекратить поддержку версии для Electron:
Люди просили приложение на Electron с самого начала, в основном потому, что хотели открывать файлы двойным щелчком. Мы также планировали разместить приложение в магазинах приложений. Параллельно кто-то предложил создать PWA, поэтому мы просто сделали и то, и другое. К счастью, мы познакомились с API проекта Fugu, такими как доступ к файловой системе, доступ к буферу обмена, работа с файлами и многое другое. Одним щелчком мыши вы можете установить приложение на свой компьютер или мобильный телефон, без дополнительных функций Electron. Решение прекратить поддержку версии на Electron, сосредоточиться только на веб-приложении и сделать его лучшим из возможных PWA, было простым. Более того, теперь мы можем публиковать PWA в Play Store и Microsoft Store! Это просто потрясающе!
Можно сказать, что Excalidraw для Electron устарел не потому, что Electron плох, вовсе нет, а потому, что веб стал достаточно хорош. Мне это нравится!
Обработка файлов
Когда я говорю, что «Интернет стал достаточно хорош», это из-за таких функций, как готовящаяся к выходу функция File Handling.
Это обычная установка macOS Big Sur. Теперь посмотрите, что происходит, когда я щёлкаю правой кнопкой мыши по файлу Excalidraw. Я могу открыть его с помощью Excalidraw, установленного PWA. Конечно, двойной щелчок тоже подойдёт, просто это менее эффектно для демонстрации на скринкасте.
Как это работает? Первый шаг — сделать типы файлов, которые может обрабатывать моё приложение, известными операционной системе. Я делаю это в новом поле file_handlers
в манифесте веб-приложения. Его значение — массив объектов со свойством action и accept
. Действие определяет URL-путь, по которому операционная система запускает ваше приложение, а объект 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
содержит поле с именем files , которое возвращает массив дескрипторов файлов для работы. Меня интересует только первый дескриптор, и из этого дескриптора я получаю двоичный объект (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 });
});
});
}
Опять же, если всё прошло слишком быстро, вы можете прочитать больше об API обработки файлов в моей статье . Вы можете включить обработку файлов, установив флажок экспериментальных функций веб-платформы. Ожидается, что эта функция появится в Chrome в конце этого года.
Интеграция с буфером обмена
Ещё одна интересная функция Excalidraw — интеграция с буфером обмена. Я могу скопировать весь рисунок или его часть в буфер обмена, при желании добавив водяной знак, а затем вставить его в другое приложение. Кстати, это веб-версия приложения Paint для Windows 95.
Принцип работы на удивление прост. Мне нужен только холст в виде BLOB-объекта, который я затем записываю в буфер обмена, передавая одноэлементный массив с ClipboardItem
и BLOB-объектом в функцию navigator.clipboard.write()
. Подробнее о возможностях API буфера обмена см. в статье Джейсона и моей .
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);
}
});
};
Сотрудничество с другими
Поделиться URL-адресом сеанса
Знаете ли вы, что в Excalidraw также есть режим совместной работы? Разные люди могут работать вместе над одним документом. Чтобы начать новый сеанс, я нажимаю кнопку совместной работы в режиме реального времени и затем запускаю сеанс. Я могу легко поделиться URL-адресом сеанса со своими соавторами благодаря API Web Share , интегрированному в Excalidraw.
Живое сотрудничество
Я смоделировал сеанс совместной работы локально, работая над логотипом Google I/O на моём Pixelbook, телефоне Pixel 3a и iPad Pro. Вы видите, что изменения, внесённые мной на одном устройстве, отражаются на всех остальных.
Я даже вижу, как перемещаются все курсоры. Курсор Pixelbook движется плавно, так как управляется трекпадом, но курсор телефона Pixel 3a и курсор планшета iPad Pro прыгают, так как я управляю этими устройствами касанием пальца.
Просмотр статусов соавторов
Для улучшения совместной работы в режиме реального времени даже работает система определения состояния бездействия. Курсор iPad Pro при использовании устройства отображается зелёной точкой. Эта точка становится чёрной при переключении на другую вкладку браузера или приложение. А когда я работаю в приложении Excalidraw, но ничего не делаю, курсор отображается как состояние бездействия, обозначенное тремя буквами zZZ.
Постоянные читатели наших публикаций могут подумать, что обнаружение простоя реализовано через Idle Detection API — раннюю версию, над которой работали в рамках проекта Fugu. Внимание, спойлер: это не так. Хотя у нас была реализация на основе этого API в Excalidraw, в итоге мы решили выбрать более традиционный подход, основанный на измерении перемещения указателя и видимости страницы.
Мы отправили отзыв о том, почему API обнаружения простоя не решал нашу задачу. Все API проекта Fugu разрабатываются открыто, так что каждый может принять участие и высказать своё мнение!
Липис о том, что сдерживает Excalidraw
Кстати, я задал lipis последний вопрос относительно того, чего, по его мнению, не хватает в веб-платформе и что сдерживает Excalidraw:
File System Access API — это здорово, но знаете что? Большинство файлов, которые мне сейчас дороги, хранятся в Dropbox или Google Drive, а не на жёстком диске. Хотелось бы, чтобы File System Access API включал уровень абстракции для интеграции с удалёнными поставщиками файловых систем, такими как Dropbox или Google, и для разработки кода. Тогда пользователи могли бы быть спокойны, зная, что их файлы в безопасности у облачного провайдера, которому они доверяют.
Полностью согласен с lipis, я тоже живу в облаке. Надеюсь, это скоро реализуют.
Режим вкладок приложения
Ух ты! Мы видели множество действительно отличных интеграций API в Excalidraw. Файловая система , работа с файлами , буфер обмена , общий доступ в Интернете и целевой ресурс общего доступа в Интернете . Но есть ещё кое-что. До сих пор я мог редактировать только один документ одновременно. Теперь это невозможно. Впервые оцените раннюю версию режима вкладок в Excalidraw. Вот как это выглядит.
У меня открыт файл в установленном Excalidraw PWA, работающем в автономном режиме. Теперь я открываю новую вкладку в отдельном окне. Это не обычная вкладка браузера, а вкладка PWA. В этой новой вкладке я могу открыть дополнительный файл и работать с ним независимо из того же окна приложения.
Режим вкладок в приложениях находится на ранней стадии развития, и пока ещё не всё окончательно решено. Если вам интересно, обязательно ознакомьтесь с текущим статусом этой функции в моей статье .
Закрытие
Чтобы быть в курсе этой и других функций, обязательно следите за нашим трекером API Fugu . Мы очень рады развивать веб и предоставлять вам больше возможностей на платформе. Желаем вам постоянно совершенствующегося Excalidraw и всех замечательных приложений, которые вы создадите! Начните творить на excalidraw.com .
Мне не терпится увидеть, как некоторые из показанных мной сегодня API появятся в ваших приложениях. Меня зовут Том, вы можете найти меня в Твиттере и в интернете как @tomayac . Большое спасибо за просмотр! Приятного просмотра на Google I/O!