Легаси-код и рефакторинг

Hyperapp — минималистичный фронтенд-фреймворк для построения интерфейсов на JavaScript, сочетающий декларативный подход с функциональной реактивностью. Центральными концепциями являются state, actions и view. State хранит текущее состояние приложения, actions изменяют состояние, а view описывает структуру интерфейса на основе состояния.

Пример базового приложения:

import { h, app } from "hyperapp";

const state = { count: 0 };

const actions = {
  increment: value => state => ({ count: state.count + value }),
  decrement: value => state => ({ count: state.count - value })
};

const view = (state, actions) => (
  h("div", {}, [
    h("h1", {}, state.count),
    h("button", { onclick: () => actions.increment(1) }, "Increment"),
    h("button", { onclick: () => actions.decrement(1) }, "Decrement")
  ])
);

app({ init: state, view, actions, node: document.getElementById("app") });

Ключевые моменты:

  • h — функция для создания виртуальных DOM-элементов.
  • app — точка инициализации приложения, связывает state, actions и view.
  • actions реализуются как функции, возвращающие новую часть состояния.

Управление состоянием и иммутабельность

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

Пример с вложенным состоянием:

const state = {
  user: { name: "Alice", age: 25 },
  loggedIn: false
};

const actions = {
  setName: name => state => ({ user: { ...state.user, name } }),
  toggleLogin: () => state => ({ loggedIn: !state.loggedIn })
};

Использование спред-оператора ... позволяет создавать новые объекты без изменения исходного состояния.


Компонентный подход

Hyperapp поддерживает разбиение интерфейса на модули и подкомпоненты, которые принимают state и actions как аргументы. Это облегчает рефакторинг и повторное использование кода.

Пример компонента:

const Counter = ({ count, increment, decrement }) =>
  h("div", {}, [
    h("h2", {}, count),
    h("button", { onclick: () => increment(1) }, "+"),
    h("button", { onclick: () => decrement(1) }, "-")
  ]);

const view = (state, actions) =>
  h("div", {}, [
    h("h1", {}, "Counters"),
    Counter({ count: state.count, increment: actions.increment, decrement: actions.decrement })
  ]);

Ключевые моменты:

  • Компоненты — это функции, возвращающие элементы виртуального DOM.
  • Передача state и actions через props обеспечивает изоляцию логики.

Работа с эффектами

Hyperapp позволяет управлять побочными эффектами через effects. Эффекты описываются как функции, которые получают аргументы и state, а затем могут вызывать внешние API, таймеры, локальное хранилище и т. д.

Пример эффекта для HTTP-запроса:

const fetchData = url => (state, actions) => {
  fetch(url)
    .then(res => res.json())
    .then(data => actions.setData(data));
};

const actions = {
  setData: data => () => ({ data }),
  loadData: url => fetchData(url)
};

Эффекты позволяют вынести асинхронную логику из компонентов и actions, что делает код более чистым и тестируемым.


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

При рефакторинге существующего Hyperapp-кода важно учитывать следующие принципы:

  1. Изоляция изменений: новые функции и компоненты создаются отдельно, минимально влияя на текущий код.
  2. Декомпозиция: большие actions и view разбиваются на маленькие, легко тестируемые функции.
  3. Использование иммутабельности: любое изменение состояния через return нового объекта упрощает отслеживание багов.
  4. Введение модульной структуры: разделение логики по папкам components, actions, effects, state улучшает читаемость.

Пример преобразования монолитного action:

До:

const actions = {
  updateUser: (name, age) => state => {
    state.user.name = name;
    state.user.age = age;
    localStorage.setItem("user", JSON.stringify(state.user));
    return state;
  }
};

После:

const actions = {
  setUser: user => () => ({ user }),
  persistUser: user => () => {
    localStorage.setItem("user", JSON.stringify(user));
  },
  updateUser: (name, age) => (state, actions) => {
    const user = { ...state.user, name, age };
    actions.setUser(user);
    actions.persistUser(user);
  }
};

Такой подход обеспечивает чистоту действий, упрощает тестирование и облегчает поддержку.


Оптимизация производительности

Hyperapp уже очень легковесен, но при работе с крупными приложениями важно следить за:

  • Минимизацией лишних рендеров: разбивать большие view на мелкие компоненты.
  • Использованием ключей в списках: h("li", { key: item.id }, item.name).
  • Чистыми функциями: state и actions должны оставаться предсказуемыми.

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

Hyperapp может сочетаться с современными инструментами:

  • TypeScript: добавляет строгую типизацию state и actions.
  • Vite/Webpack: ускоряет сборку и HMR.
  • Тестирование: функции view и actions можно тестировать как чистые функции без браузера.

Пример тестирования action с Jest:

test("increment increases count", () => {
  const state = { count: 1 };
  const newState = actions.increment(2)(state);
  expect(newState.count).toBe(3);
});

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