javascript

GridStack: цена выбора API для nested grids

  • четверг, 20 августа 2026 г. в 00:00:08
https://habr.com/ru/articles/1071556/

Иногда сложные инженерные проблемы возникают не из‑за ошибок в библиотеке или коде. Причина может быть в том, что библиотека используется не оптимально для текущих архитектурных условий.

Всё началось с задачи в секции дашбордов и виджетов. Секция ранее была переведена на библиотеку GridStack. Нужно было использовать функцию GridStack nested grids — размещение одной сетки внутри другой. Для пользователя это возможность создать новый виджет‑группу, внутри которого можно размещать другие виджеты. То есть еще один маленький дашборд внутри общего. Грид внутри грида. 

При этом грид внутри папки должен был быть таким же функциональным, как общий (с ресайзом и drag’n’drop), плюс дополнительные функции — папка подстраивается размером под детей, сворачивается, может быть запинена наверху общего дашборда. И главное — мост между ними, возможность drag'n'drop виджетов между гридами.

На первый взгляд задача проста — создать GridStack внутри GridStack и связать их между собой. Для создания группы я выбрала GridStack.addGrid(). Этот API выглядел логичным выбором для нашей компонентной модели: у «группы» был собственный Vue‑компонент, а внутри него требовалось создать собственную сетку. API позволял сделать именно это, и первые сценарии действительно работали.

Примерно так выглядела структура. Group Widget - Vue компонент виджета, внутри которого мы вызываем addGrid()
Примерно так выглядела структура. Group Widget — Vue компонент виджета, внутри которого мы вызываем addGrid()
// создаем GridStack вручную внутри vue-компонента

const subGrid = GridStack.addGrid(element, {
  children: [...],
  acceptWidgets: true,
  dragOut: true,
})

Первые сценарии работали корректно, на этапе создания nested grid все работало стабильно. Но проблемы начались позже, когда между root и nested grid стало требоваться большое количество сложных взаимодействий. И чем больше их становилось, тем менее стабильным становилось поведение.

Малейшая неточность в расчете позиции, малейшая задержка при сохранении  - все начинало порождать “снежный ком” багов, который копился без перерисовки страницы.
Малейшая неточность в расчете позиции, малейшая задержка при сохранении — все начинало порождать «снежный ком» багов, который копился без перерисовки страницы.

Интерфейс просто начинал «расходиться» с реальным состоянием. Получался уже «снежных ком» дебага — редактируя одну часть процесса, часто я задевала другую.

Я увидела глобальную проблему в том, что в итоге много процессов управляются методами вручную, а не силами библиотеки.
Для самой библиотеки это были два независимых экземпляра GridStack. Каждый экземпляр грида самостоятельно:

  • рассчитывал layout;

  • отслеживал события;

  • управлял drag‑and‑drop;

  • принимал решения о размещении элементов.

А мост для процессов между ними был выстроен вручную.

Я начала заново изучать документацию GridStack и примеры nested grids. Стало понятно, что библиотека предлагает два разных способа создания вложенной сетки.

Первый — это, собственно, GridStack.addGrid(), который создаёт новый экземпляр GridStack на существующем DOM‑элементе.

Второй — вместо создания отдельного GridStack вложенная сетка описывалась как часть дерева основного layout через subGridOpts при вызове grid.load().
Компонент виджета‑группы больше не создает GridStack, а лишь описывает свойства.

Архитектурно это уже выглядело иначе:

Здесь видно кардинальное отличие от первого подхода - один общий владелец, nested grid стал частью дерева основного GridStack. А Vue-компонент выполняет только визуальные функции, а не управляет процессами в гридах и их взаимодействием.
Здесь видно кардинальное отличие от первого подхода — один общий владелец, nested grid стал частью дерева основного GridStack. А Vue‑компонент выполняет только визуальные функции, а не управляет процессами в гридах и их взаимодействием.
// Формируем описание одного элемента layout.
// Если это обычный виджет — GridStack создаст обычную ячейку.
// Если это группа — дополнительно описываем вложенную сетку.

const node = {
  id: group.id,
  x: 0,
  y: 0,
  w: 24,
  h: 2,

  // Группа автоматически меняет высоту
  // в зависимости от содержимого
  sizeToContent: !collapsed,

  // Вместо создания второго GridStack
  // просто описываем параметры вложенной сетки
  subGridOpts: {
    children: group.children,

    acceptWidgets: canDropIntoGroup,

    dragOut: !collapsed,

    disableDrag: collapsed,

    disableResize: collapsed,
  }
}

// Передаем описание всего layout библиотеке.
// Дальше GridStack сам:
// • создаст nested grid;
// • свяжет его с родительским;
// • настроит native drag'n'drop между сетками;
// • будет управлять его жизненным циклом.
grid.load([node])

В этом случае библиотека брала на себя «мост» между root и nested grid. И это означало, что мне больше не нужно было самостоятельно поддерживать связь между двумя независимыми экземплярами.

Я отказалась от прежнего подхода, отказалась от недель работы, и начала реализацию заново с новым API.

Индикатором, что это решение работает, стало то, что native drag-and-drop между гридами заработал стабильно сразу же.
Больше не требовалось так много фиксов и ручных методов, кода стало значительно меньше, а работа стала значительно стабильнее.

Было

Стало

отдельный lifecycle sub‑grid

lifecycle управляет GridStack

ручная синхронизация DnD

native cross‑grid DnD

ручной расчёт высоты под содержимое

sizeToContent

собственный drop preview

встроенный механизм GridStack

два источника событий

единый event pipeline

Что хотелось бы сказать по итогу.

  1. GridStack.addGrid() не является плохим или неправильным API.

    Но в нашей архитектуре более естественной моделью оказался subGridOpts, где nested grid — часть дерева основной сетки.

    При использовании subGridOpts вложенная сетка становится частью дерева основного GridStack.
    Библиотека знает о вложенности этих сеток и сама предоставляет механизмы для их взаимодействия: создаёт nested grid, поддерживает cross-grid drag-and-drop, связывает события с outermost grid и позволяет управлять размером через sizeToContent

    В случае addGrid() такой связи нет — существуют два независимых экземпляра GridStack, а вся логика взаимодействия остается на стороне приложения.

    Для моего случая и аналогичных — все сводится к идее: "Сначала модель layout и её владелец, а уже поверх неё — UI-компонент. Не наоборот"

  2. К сожалению, иногда лучший фикс — удалить код. А чистое решение — это начать сначала.

    Это было самым сложным для меня. Признать, что подход, в который уже было вложено несколько недель работы, не станет стабильным.

    Отказаться от тысяч строк уже написанного кода психологически тяжело. Особенно когда этот код действительно работает в простых сценариях, а проблемы проявляются только после длинной цепочки действий пользователя. Кажется, вот еще один фикс, и все...

    Но в итоге я поняла, что даже если удастся стабилизировать поведение — какой ценой? Код должен быть прост, понятен, поддерживаем.

  3. Ретроспективно я бы потратила больше времени на сравнение двух способов создания nested grid до начала реализации.
    Но это не отменяет ценности полученного опыта: именно на реальных сценариях стало видно, какая модель лучше соответствует архитектуре нашего приложения.

Не весь написанный код является прогрессом. Иногда прогресс — это понять, какой код больше не должен существовать.