Learn JavaScript

Как разложить код по файлам: data, logic, ui

Когда код растет, главная проблема - не синтаксис, а “где что лежит”. Если перемешать работу с данными, логику и DOM, появляется эффект “трогаешь одно - ломается другое”.

Один из самых простых и рабочих подходов для небольших проектов - разделить код на три слоя:

  • data - получение и хранение данных
  • logic - правила и вычисления
  • ui - работа с DOM и отображение

1) Что относится к data

Слой data отвечает за:

  • загрузку данных (fetch)
  • сохранение/чтение (localStorage)
  • преобразование “сырого ответа” в удобный формат (если нужно)
  • кеширование

data не должен знать про DOM.

Примеры функций data:

  • loadJSON(url)
  • saveSettings(settings)
  • loadSettings()
  • loadWithCache(url, options)

2) Что относится к logic

Слой logic отвечает за:

  • чистые вычисления
  • правила (фильтрация, сортировка, валидация)
  • подготовку данных к отображению

logic не должен:

  • читать DOM
  • менять DOM
  • делать fetch
  • писать в localStorage

Примеры функций logic:

  • formatPrice(value)
  • filterActive(items)
  • buildViewModel(data)

3) Что относится к ui

Слой ui отвечает за:

  • поиск элементов (querySelector)
  • обработку событий (addEventListener)
  • обновление разметки (render)
  • отображение ошибок и загрузки

ui не должен:

  • держать “правила” внутри обработчиков
  • вычислять сложные вещи
  • смешивать сетевую логику с DOM-логикой в одной функции

4) Простое правило зависимостей

Чтобы структура не превратилась в хаос, помогает правило направленности:

  • ui может импортировать logic и data
  • logic может импортировать маленькие утилиты (внутри logic), но не ui и не data
  • data может импортировать утилиты (например, safeJSONParse), но не ui и не logic

Если logic начнет импортировать ui, получится круговая зависимость и ломается читаемость.

5) Шаблон структуры папок

Один из вариантов:

src/
  main.js
  data/
    api.js
    storage.js
    cache.js
  logic/
    calc.js
    format.js
  ui/
    dom.js
    render.js

Это не “единственно верно”, но дает понятный ориентир.

6) Мини-пример: список товаров с фильтром

Ниже пример того же функционала, но разделенного по слоям.

data/api.js

export async function loadJSON(url) {
  const response = await fetch(url);

  if (!response.ok) {
    throw new Error("HTTP " + response.status);
  }

  return response.json();
}

logic/format.js

export function formatPrice(value) {
  return value.toFixed(2);
}

logic/filter.js

export function filterByMinPrice(items, minPrice) {
  return items.filter((item) => item.price >= minPrice);
}

ui/render.js

import { formatPrice } from "../logic/format.js";

export function renderList(root, items) {
  root.innerHTML = "";

  for (const item of items) {
    const li = document.createElement("li");
    li.textContent = item.title + " - " + formatPrice(item.price);
    root.append(li);
  }
}

export function renderError(root, message) {
  root.textContent = message;
}

main.js (точка входа)

import { loadJSON } from "./data/api.js";
import { filterByMinPrice } from "./logic/filter.js";
import { renderList, renderError } from "./ui/render.js";

const listEl = document.querySelector("#list");
const inputEl = document.querySelector("#minPrice");
const reloadBtn = document.querySelector("#reload");

let items = [];

async function loadAndRender() {
  try {
    items = await loadJSON("/data/items.json");
    render();
  } catch (e) {
    renderError(listEl, "Load failed");
  }
}

function render() {
  const minPrice = Number(inputEl.value) || 0;
  const filtered = filterByMinPrice(items, minPrice);
  renderList(listEl, filtered);
}

inputEl.addEventListener("input", render);
reloadBtn.addEventListener("click", loadAndRender);

loadAndRender();

Что видно по структуре:

  • main.js собирает проект: получает DOM-элементы, подписывается на события, связывает data -> logic -> ui
  • data занимается только запросами
  • logic только фильтрует и форматирует
  • ui только рисует

7) Как понять, что файл “не на своем месте”

Несколько быстрых сигналов:

  • файл ui внезапно содержит сложные вычисления и десятки строк условий
  • функция logic читает document.querySelector
  • функция data возвращает готовый HTML
  • обработчик события делает fetch, парсинг, фильтрацию, сортировку и render в одном месте

В таких случаях код обычно проще разделить на маленькие функции и вынести по слоям.

Что стоит запомнить

  • data - про загрузку и хранение, logic - про правила и вычисления, ui - про DOM.
  • Направление зависимостей важно: ui -> logic -> utils, ui -> data -> utils.
  • Точка входа (main) склеивает слои и управляет потоком.
  • Такой подход помогает быстрее искать ошибки и легче дополнять проект.