Перетаскивание элементов в веб-интерфейсах (Drag and Drop) — одна из тех задач, за которые фронтендеры берутся с тяжелым вздохом. Нативный браузерный HTML5 Drag and Drop API создавался в эпоху десктопов и до сих пор плохо приспособлен для мобильных тач-экранов: фантомное изображение (drag image) сложно стилизовать, прозрачность ведет себя непредсказуемо, а анимации сдвига соседних элементов приходится писать вручную.
В экосистеме React признанным стандартом давно стал пакет @dnd-kit. Для Vue разработчик под ником ZiZIGY создал порт vue-dnd-kit, который быстро набрал тысячи звезд. Но по мере роста проектов вторая версия библиотеки зашла в архитектурный тупик.
Вместо бесконечных патчей автор принял радикальное решение: полностью снести старую кодовую базу и выпустить библиотеку Vue DnD Kit версии 3.0 с нуля. Давай разберем разбор на Хабре и посмотрим, как устроена новая архитектура.
Архитектурный тупик версии 2: 57 багов, layout thrashing и сломанный TypeScript
Вторая версия библиотеки страдала от слишком низкого уровня абстракций. Разработчику предоставлялись базовые примитивы makeDraggable и makeDroppable. Чтобы собрать обычный список задач с сортировкой, приходилось писать десятки строк обвязки: вручную отслеживать индексы, слушать событие onDrop, вызывать служебную функцию suggestSort, мутировать массив и заворачивать элементы в компонент <TransitionGroup> ради хоть какой-то плавности.
Аудит кодовой базы вскрыл целый ворох фундаментальных проблем:
- Алгоритмическая сложность $O(N^2)$: поиск зоны сброса (droppable-контейнера) под курсором выполнялся наивным перебором всех зарегистрированных элементов на странице. На каждом кадре движения мыши вызывался метод
getBoundingClientRect(), что вызывало принудительный пересчет геометрии (layout thrashing) и роняло частоту кадров со 120 FPS до дерганых 25 FPS на длинных списках. - 57 скрытых дефектов ядра: любое событие
mouseupна документе сбрасывало глобальные CSS-свойстваuser-selectиtouch-action, задержка активацииactivationDelayв миллисекундах ошибочно умножалась как секунды, а в файлах типов.d.tsключевые дженерики незаметно превращались в типany.
Попытка опереться на чужое низкоуровневое ядро @dnd-kit/dom не увенчалась успехом: сторонний движок требовал императивных команд через метод move() и не умел использовать нативную реактивность Vue 3.5+. Выход оставался один — написать собственный специализированный движок.
Концепция реактивного черновика: почему бизнес-данные больше не страдают при срыве жеста
Главная архитектурная находка Vue DnD Kit 3.0 — концепция реактивного черновика (Draft State).
Раньше библиотека напрямую мутировала исходный массив элементов в процессе перетаскивания. Если пользователь тащил карточку над списком, элементы менялись местами в памяти. Но что происходило, если в этот момент палец сорвался с экрана, пользователь нажал клавишу Escape или отпустил курсор над запрещенной зоной? Начинался откат: массив приходилось возвращать в исходное состояние, что вызывало мерцание интерфейса и лишние триггеры реактивных наблюдателей (watchers).
В композабле makeSortableList массив items является реактивным черновиком:
- Исходный массив задач в твоем
refили Pinia Store остается абсолютно нетронутым во время всего процесса перетаскивания. - Библиотека манипулирует только черновиком: соседние карточки расступаются, анимация показывает будущее положение элемента, но в бизнес-логике никаких изменений не происходит.
- Если жест отменяется — черновик мгновенно сбрасывается без побочных эффектов.
- Если пользователь подтвердил сброс (Drop), итоговый порядок атомарно записывается в бизнес-состояние.
Вот как лаконично выглядит базовый сортируемый список на Vue 3.5+:
<script setup lang="ts">
import { ref, useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
interface Task {
id: string;
title: string;
}
const listRef = useTemplateRef<HTMLElement>('listRef');
const tasks = ref<Task[]>([
{ id: '1', title: 'Спроектировать схему PostgreSQL' },
{ id: '2', title: 'Настроить BDD-тесты Playwright' },
{ id: '3', title: 'Обновить переменные Claude Code' }
]);
// makeSortableList возвращает черновик items для отрисовки
const { items } = makeSortableList(listRef, {
items: tasks,
id: 'tasks-sortable-list'
});
</script>
<template>
<ul ref="listRef" class="task-list">
<li
v-for="task in items"
:key="task.id"
:data-dnd-id="task.id"
class="task-item"
>
{{ task.title }}
</li>
</ul>
</template>
Обрати внимание: никакого компонента <TransitionGroup>, никаких ручных вызовов функций сортировки. Библиотека сама следит за DOM через шаблонные ссылки useTemplateRef и атрибут data-dnd-id.
Встроенная кинематика без TransitionGroup: канбан-доски и деревья из коробки
Вторая боль разработчиков — физика движения. Настройка плавного скольжения элементов по алгоритму FLIP (First, Last, Invert, Play) обычно отнимала дни работы.
В версии 3.0 кинематика встроена в само ядро:
- Сдвиг соседних карточек рассчитывается математически без создания промежуточных узлов DOM;
- Возврат карточки на исходное место при нажатии
Escapeпроисходит с плавной физической интерполяцией; - Движок автоматически проверяет системную настройку
prefers-reduced-motion: если у пользователя включен режим пониженного движения, анимации мгновенно отключаются ради комфорта и доступности.
Для построения канбан-досок больше не нужно городить костыли с синхронизацией событий. Достаточно объединить списки одним строковым идентификатором группы:
import { useTemplateRef } from 'vue';
import { makeSortableList } from '@vue-dnd-kit/core';
import { useKanbanStore } from '@/stores/kanban';
const kanbanStore = useKanbanStore();
const columnRef = useTemplateRef<HTMLElement>('columnRef');
const { items } = makeSortableList(columnRef, {
id: 'column-review',
group: 'project-kanban-board',
items: () => kanbanStore.columns['review'],
onDrop: (event) => {
event.preventDefault();
// Атомарно обновляем колонки в сторе
for (const [columnId, newItems] of event.result) {
kanbanStore.updateColumn(columnId, newItems);
}
}
});
Кроме того, библиотека из коробки научилась работать с иерархическими деревьями: сдвиг перетаскиваемого узла вправо автоматически меняет уровень вложенности с учетом настраиваемого лимита глубины.
Как мы недавно видели, изучая дайджест экосистемы Vue и Nuxt, переход фронтенд-инструментов на строгие реактивные примитивы Vue 3.5 позволяет выкидывать мегабайты старого кода, делая интерфейсы отзывчивыми и стабильными.
Симбиоз с Claude Code: как AI-агенты помогли переписать библиотеку с нуля
Интересна и сама инженерная кухня релиза 3.0. Автор открыто рассказывает, что переписывание библиотеки велось в тесной связке с консольным агентом Claude Code:
- Агент провел сплошной аудит старой кодовой базы, сопоставил сотни открытых issue на GitHub с исходным кодом и составил реестр из тех самых 57 критических багов.
- Агент исследовал архитектуру конкурирующих решений (Pragmatic Drag and Drop от Atlassian) и помог сформулировать черновик технического RFC.
- В параллельных терминальных сессиях агент генерировал десятки стресс-тестов на граничные условия: резкий рывок мыши за пределы экрана, мультитач на мобильных устройствах, поведение при активном скролле страницы.
При этом продуктовые решения — дизайн публичного API, отказ от внешних зависимостей и семантика работы черновиков — оставались за человеком.
Vue DnD Kit 3.0 наглядно показывает, как зрелая фронтенд-разработка избавляется от многолетних костылей, превращая сложнейшие интерфейсные взаимодействия в пару строк чистого декларативного кода.
