Рефакторинг легаси кода

Stimulus — JavaScript-фреймворк, ориентированный на усиление существующей HTML-разметки без её радикальной перестройки. Эта особенность делает его особенно подходящим для работы с легаси-кодом, где уже присутствует серверный рендеринг, разрозненные скрипты, inline-обработчики и непоследовательная архитектура.

В контексте рефакторинга ключевая ценность Stimulus заключается в инкрементальности: код можно улучшать по частям, не переписывая приложение целиком и не нарушая текущую функциональность.


Типичное состояние легаси JavaScript

Легаси-код в браузере обычно характеризуется следующими признаками:

  • inline-обработчики (onclick, onchange);
  • глобальные функции и переменные;
  • прямые document.querySelector и addEventListener в разных файлах;
  • отсутствие явной связи между HTML и JavaScript;
  • повторяющаяся логика для схожих компонентов;
  • сложность тестирования и сопровождения.

Пример характерного фрагмента:

<button oncl ick="toggleMenu()">Меню</button>

<script>
function toggleMenu() {
  var menu = document.getElementById('menu');
  menu.classList.toggle('open');
}
</script>

Такой код работает, но масштабируется плохо и быстро теряет управляемость.


Принцип «HTML как источник истины»

Stimulus строится вокруг идеи, что HTML уже содержит всю необходимую информацию о поведении интерфейса. JavaScript не управляет DOM напрямую, а подключается к нему декларативно.

Основные концепции:

  • data-controller — привязка логики к DOM-элементу;
  • data-action — декларативное описание событий;
  • data-target — именованные ссылки на элементы внутри контроллера;
  • values и classes — конфигурация и состояние через атрибуты.

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


Первый шаг рефакторинга: изоляция поведения

Начальный этап — перенос логики из глобальной области видимости в Stimulus-контроллер без изменения внешнего поведения.

Исходное состояние:

<button id="menuButton">Меню</button>
<div id="menu"></div>

<script>
document.getElementById('menuButton').addEventListener('click', function () {
  document.getElementById('menu').classList.toggle('open');
});
</script>

Рефакторинг с использованием Stimulus:

<div data-controller="menu">
  <button data-action="click->menu#toggle">Меню</button>
  <div data-menu-target="panel"></div>
</div>
import { Controller } from "@hotwired/stimulus";

export default class extends Controller {
  static targets = ["panel"];

  toggle() {
    this.panelTarget.classList.toggle("open");
  }
}

Поведение осталось тем же, но:

  • исчезли глобальные обработчики;
  • логика изолирована;
  • структура стала самодокументируемой.

Постепенная декомпозиция скриптов

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

Стратегия:

  1. Выделить независимые фрагменты поведения.
  2. Для каждого фрагмента создать отдельный контроллер.
  3. Подключать контроллеры только там, где они действительно нужны.

Пример: один файл управляет модальными окнами, табами и тултипами. Каждый из этих элементов может быть вынесен в собственный контроллер (modal, tabs, tooltip) без влияния на остальной код.


Работа с существующей разметкой

Stimulus не требует перестройки HTML. Атрибуты data-* добавляются поверх существующей структуры.

Пример легаси-шаблона:

<ul class="tabs">
  <li class="active">Профиль</li>
  <li>Настройки</li>
</ul>

Минимальный рефакторинг:

<ul class="tabs" data-controller="tabs">
  <li data-action="click->tabs#select" data-tabs-target="tab" class="active">Профиль</li>
  <li data-action="click->tabs#select" data-tabs-target="tab">Настройки</li>
</ul>

Контроллер берет на себя управление состоянием, не меняя CSS и визуальное представление.


Удаление jQuery без «большого взрыва»

Во многих легаси-проектах Stimulus используется как мост при уходе от jQuery.

Типичный jQuery-код:

$('.dropdown-toggle').on('click', function () {
  $(this).next('.dropdown-menu').toggle();
});

Эквивалент на Stimulus:

<div data-controller="dropdown">
  <button data-action="click->dropdown#toggle">...</button>
  <div data-dropdown-target="menu"></div>
</div>
toggle() {
  this.menuTarget.classList.toggle("hidden");
}

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


Управление состоянием через values

Легаси-код часто хранит состояние в DOM, глобальных переменных или замыканиях. Stimulus предоставляет механизм values, который делает состояние явным и типизированным.

<div
  data-controller="counter"
  data-counter-count-value="0">
</div>
static values = { count: Number };

increment() {
  this.countValue++;
}

Состояние:

  • синхронизировано с DOM;
  • легко отлаживается;
  • не требует внешних хранилищ.

Предсказуемый жизненный цикл

Stimulus автоматически управляет жизненным циклом контроллеров:

  • connect — элемент появился в DOM;
  • disconnect — элемент удалён;
  • initialize — создание экземпляра.

Это особенно важно при работе с легаси-кодом, где DOM может динамически меняться (AJAX, Turbo, server-side рендеринг).

Вместо ручного пересоздания обработчиков:

connect() {
  this.startObserving();
}

disconnect() {
  this.stopObserving();
}

Код становится устойчивым к перерисовкам и частичным обновлениям страницы.


Интеграция с серверным рендерингом

Stimulus органично работает в приложениях без SPA-архитектуры. Это критично для легаси-систем, построенных вокруг серверных шаблонов.

Особенности:

  • контроллеры активируются автоматически после загрузки HTML;
  • не требуется виртуальный DOM;
  • можно усиливать отдельные участки страницы.

Это позволяет улучшать UX без изменения архитектурных основ проекта.


Рефакторинг как непрерывный процесс

Использование Stimulus переводит рефакторинг из разряда «переписать всё» в режим постоянного улучшения:

  • новый код сразу пишется структурированно;
  • старый код постепенно изолируется и удаляется;
  • архитектура выравнивается со временем.

В результате легаси-проект перестаёт быть «замороженным» и снова становится пригодным для развития без резких и рискованных изменений.