Дайджесты новостей
Динамичная европейская комиксная иллюстрация процесса интерактивного перетаскивания карточки задачи с реактивным черновиком во Vue DnD Kit 3.0.

Vue DnD Kit 3.0: полная переработка библиотеки Drag & Drop для Vue 3

Перетаскивание элементов в веб-интерфейсах (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 является реактивным черновиком:

  1. Исходный массив задач в твоем ref или Pinia Store остается абсолютно нетронутым во время всего процесса перетаскивания.
  2. Библиотека манипулирует только черновиком: соседние карточки расступаются, анимация показывает будущее положение элемента, но в бизнес-логике никаких изменений не происходит.
  3. Если жест отменяется — черновик мгновенно сбрасывается без побочных эффектов.
  4. Если пользователь подтвердил сброс (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:

  1. Агент провел сплошной аудит старой кодовой базы, сопоставил сотни открытых issue на GitHub с исходным кодом и составил реестр из тех самых 57 критических багов.
  2. Агент исследовал архитектуру конкурирующих решений (Pragmatic Drag and Drop от Atlassian) и помог сформулировать черновик технического RFC.
  3. В параллельных терминальных сессиях агент генерировал десятки стресс-тестов на граничные условия: резкий рывок мыши за пределы экрана, мультитач на мобильных устройствах, поведение при активном скролле страницы.

При этом продуктовые решения — дизайн публичного API, отказ от внешних зависимостей и семантика работы черновиков — оставались за человеком.

Vue DnD Kit 3.0 наглядно показывает, как зрелая фронтенд-разработка избавляется от многолетних костылей, превращая сложнейшие интерфейсные взаимодействия в пару строк чистого декларативного кода.