Demo | GitHub
Эксперименты с созданием редактора диаграмм на Blazor Webassembly (Blazor WebAssembly: Drag and Drop в SVG, Blazor WebAssembly: соединительные линии в SVG) показали что технология не годится для интенсивных манипуляций с DOM.
То что будут проседания было известно заранее: WebAssembly не имеет доступа к DOM, любые изменения только через вызовы JavaScript. Задержки оказались такими большими, что перетаскивание на мобильном тормозило уже после добавления третьей фигуры.
Отказ от фреймворков (для данной задачи)
Подозрение что Blazor-овский виртуальный DOM не корректно отслеживает изменения (может Blazor пытается обновить больше DOM-объектов чем требуется) не оправдались. Throttling событий и прочие рекомендации Microsoft (ASP.NET Core Blazor performance best practices) - не помогли.
Чтобы просто обновлять один атрибут, нужно учесть так много нюансов:
виртуальный DOM, деревья, поддеревья,
“петли” изменений (изменения в одном месте приводят к изменениям в другом, изменения там приводят к изменению в первом),
особенности передачи параметров в компоненты и отслеживания их изменений, подписки/отписки.
Тривиальная для JavaScript задача, в Blazor сделалась слишком сложной. Фреймворк здесь только мешает. Излишняя сложность проявляется не только в Blazor, но и в других фреймворках. Если еще не видели, посмотрите выступление автора Svelte “Rich Harris - Rethinking reactivity”. В видео есть пример с тормозами React приложения: там DOM перестраивается на лету при вводе в текстовое поле. Здесь DOM перестраивается на лету при движении мышки (перетаскивании фигуры).
Сделанный за 20 минут на vanilla-JavaScript прототип не показывал признаков замедления при 1000 фигур.
После нескольких лет использования Angular-а, делать что-то на vanilla-JavaScript казалось регрессом. Ладно еще вручную читать HTML атрибуты и вешать обработчики. А как без компонентов, без IoC, без шаблонов? Самое главное - без “реактивности”? Однако ломка прошла достаточно быстро. Оказалось за границами фреймворков есть жизнь, и в чем то более полноценная.
Отказ от TypeScript (от компилятора TypeScript)
Проверка типов, intellisense и прочий tooling - вот за что любят TypeScript. В TypeScript есть интерфейсы, литеральные типы и даже генерики. TypeScript так затягивает, что легко забыть что TypeScript - это же только механизм описания типов для JavaScript. Да это написано на главной странице typescriptlang.org: “TypeScript is JavaScript with syntax for types.”
Все те же возможности (проверки типов, intellisense и проч.) дает JSDoc.
Пример “типизации” с помощью JSDoc:
/**
* @param {SVGGraphicsElement} svgEl
* @param {number} transform
* @param {SVGSVGElement=} svg pass if svgEl not yet in DOM
* @returns {SVGTransform}
*/
function ensureTransform(svgEl, transform, svg) {
...
return ...;
}
Можно даже описывать типы на TypeScript и использовать их в js-файлах:
// ts-файл
interface IDiagram {
on(evtType: DiagramEventType, listener: EventListenerOrEventListenerObject): this;
shapeAdd(param: PresenterShapeAppendParam): IDiagramShape;
shapeDel(shape: IDiagramShape): void;
shapeConnect(param: DiagramShapeConnectParam): void;
}
// js-файл - Diagram реализует IDiagram
/** @implements {IDiagram} */
export class Diagram {
…
}
При этом будут работать и “find all references” и переименование и проверка что объект реализует интерфейс (по крайней мере в Visual Studio Code все заработало из коробки).
Плюсы отказа от компилятора TypeScript:
код JS именно такой, какой вы написали,
ускоряет разработку - не нужно ждать компиляции,
не нужны map-файлы, легче отлаживать.
JSDoc не так лаконичен как TypeScript, синтаксис на любителя, хуже поддержка IDE.
Удобным оказался смешанный подход:
описания типов в ts-файлах на TypeScript
реальный код на JavaScript с JSDoc.
DgrmJS
В итоге получилась vanilla-JavaScript библиотека DgrmJS.
DgrmJS - это библиотека для создания редакторов диаграмм рабочих процессов.
Особенности библиотеки:
поддержка пк и мобильных,
нет зависимостей,
малый вес,
фигуры создаются декларативно.
Основная идея:
Использовать стандартные объекты и возможности SVG для декларативного создания фигур, которые будут использоваться на диаграмме.
Чтобы создать фигуру - в стандартную разметку SVG нужно добавить специальные data-атрибуты. Таким образом, любые svg изображения можно использовать в качестве фигур диаграммы.DgrmJS отправляет события, такие как «фигура выбрана» или «фигура соединяется с другой фигурой» и другие.
Можно использовать эти события для реализации собственной логики, например: сделать JSON-описание рабочего процесса, запретить соединение фигур и т.п.
Пример декларативного описания шаблона фигуры “circle”:
<g data-templ="circle">
<circle ... />
<text data-key="text"></text>
<!--
out connector
data-connect-point - point into shape where connector line starts
data-connect-dir - direction of connector line
-->
<circle
data-connect="out"
data-connect-point="60,0"
data-connect-dir="right" ...>
</circle>
<!--
in connector
-->
<circle
data-connect="in"
data-connect-point="-60,0"
data-connect-dir="left" ...>
</circle>
</g>
На рисунке ниже показаны две фигуры (два круга), созданные по шаблону “circle”. По клику на фигуре - отображаются выходные коннекторы, откуда можно вытащить соединительную линию. При наведении конца соединительной линии на фигуру - отображаются входные коннекторы.
Код добавления фигуры в диаграмму:
import { svgDiagramCreate } from './diagram/svg-presenter/svg-diagram-fuctory.js';
const diagram = svgDiagramCreate(document.getElementById('diagram'));
diagram.shapeAdd({
templateKey: 'circle',
position: { x: 120, y: 120 }
});
Больше примеров на GitHub.
Заключение
Статья не призывает отказываться от фреймворков или TypeScript. Долгое следование одним и тем же парадигмам, подходам, фреймворкам в итоге может “зашорить”, сузить охват зрения. Часто мы даже не делаем выбор - попробуйте найти вакансию Blazor WebAssembly или Svelte, выбирать можно только между React и Angular (еще Vue).
Хорошо что есть возможность экспериментировать. Вынырнуть из пузыря “реактивного подхода” было интересно.