Skip to content

Latest commit

 

History

History
366 lines (248 loc) · 10.2 KB

File metadata and controls

366 lines (248 loc) · 10.2 KB
title Занятие 23
description Разделение логики и представления

Lesson 23

OTUS Javascript Basic

Разделение логики и представления

Разделение логики и представления (шаблонизация, сервисный слой)

Цели занятия

  • Разобрать, как разделение кода на составляющие помогает с переиспользованием кода и его поддержкой (на простых примерах с запросами и DOM).
  • Узнать подходы: принцип единственной ответственности, представление, шаблонизация, сервисный слой, MVC и увидеть, как они выражаются в коде (используя то, что уже известно: функции, async/await, fetch, DOM, события).
  • На практике: маленькие примеры и рефакторинг (без классов — только функции).

Компетенции:

  • Применение метанавыков для обработки информации и принятия решений.
  • Умение структурировать программы.

План занятия

  • Введение: Зачем разделять — как "сортировка" в коде
  • Теория: Определения и подходы.
  • Маленькие примеры с запросами и DOM: "до/после"
  • Лайвкодинг: Плохой код → шаг за шагом в MVC
  • Итоги

Введение:

Почему разделение — это легко и выгодно?

Вы уже знаете DOM, async/await и fetch, функции и объекты. Но в простых скриптах часто всё смешивается: fetch в обработчике клика + innerHTML там же. Результат — дубли, ошибки при изменении (сломал UI — сломал запрос).

Разделение — как разложить по полочкам: запросы в "сервисах" (функциях), показ — в view. Переиспользуем: тот же fetch для списка и деталей. Поддержка: меняй дизайн — логика остаётся целой.

Мотивация: редактирование и поддержка кода

Принцип единственной ответственности

Определение: каждый кусок кода выполняет одну задачу (fetch — только данные, innerHTML — только показ).

fetch в обработчике + DOM

document.getElementById("btn").addEventListener("click", async () => {
  const response = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const data = await response.json();
  document.getElementById("output").innerHTML = `<div>${JSON.stringify(
    data
  )}</div>`;
});

Плохо: данные и отображение смешаны, нет переиспользования, любая ошибка в fetch ломает UI

Представление (View)

Только отображение (innerHTML, querySelector). Не знает, откуда данные.

Пример: renderData()

renderData смешано с логикой

function renderDataAndFetch() {
  fetch("https://jsonplaceholder.typicode.com/users/1")
    .then((res) => res.json())
    .then((data) => {
      document.getElementById("output").innerHTML = `<pre>${JSON.stringify(
        data,
        null,
        2
      )}</pre>`;
    });
}
renderDataAndFetch();

Плохо: функция одновременно делает fetch и вставку в DOM, нарушается принцип единственной ответственности

Шаблонизация

Определение: генерация HTML из заготовки и данных.

Пример:

for (let i = 0; i < data.length; i++) {
  html += "<p>" + data[i] + "</p>";
}

Сервисный слой

Функции для запросов и расчётов.

Пример:

сервисный fetch встроен в обработчик

document.getElementById("btn").addEventListener("click", async () => {
  const res = await fetch("https://jsonplaceholder.typicode.com/users/1");
  const data = await res.json();
  console.log(data);
});

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

Теория: MVC

Model — данные и сервисы (fetch-функции).

View — показ (шаблоны и DOM).

Controller — связывает (addEventListener → model → view).

Преимущества: структура и масштабируемость.

Примеры: "До" и "После"

Используем fetch, addEventListener, innerHTML.

Каждый пример: "плохой" (смешанный) и "хороший" (разделённый).

Пример 1: "До"

<button id="btn">Показать</button>
<div id="name"></div>
<script>
  function showName() {
    var name = "Алекс";
    document.getElementById("name").innerHTML = "Имя: " + name;
  }
  document.getElementById("btn").addEventListener("click", showName);
</script>

Недостаток: данные и DOM в одном месте.

Пример 1: "После"

function getName() {
  return "Алекс";
}

function renderName(name) {
  document.getElementById("name").innerHTML = `Имя: ${name}`;
}

document
  .getElementById("btn")
  .addEventListener("click", () => renderName(getName()));

Разделение: данные и представление отделены.

Пример 2: "До" — список имён

const names = ["Маша", "Петя"];
let html = "<ul>";

for (let i = 0; i < names.length; i++) {
  // Здесь потенциальная уязвимость:
  // если names[i] придёт из ненадёжного источника,
  // вставка через + в innerHTML позволит выполнить HTML/JS
  html += "<li>" + names[i] + "</li>";
}

html += "</ul>";

// Здесь тоже уязвимость: innerHTML вставляет HTML напрямую
document.getElementById("list").innerHTML = html;

Недостаток: небезопасно.

Пример 2: "После" — шаблон

const names = ["Маша", "Петя"];

function renderListSafe(items, containerId) {
  const container = document.getElementById(containerId);

  // создаём новый ul
  const ul = document.createElement("ul");

  items.map((name) => {
    const li = document.createElement("li");
    li.textContent = name; // безопасно
    ul.appendChild(li);
  });

  // заменяем старое содержимое новым ul
  container.replaceChildren(ul);
}

renderListSafe(names, "list");

безопаснее

Пример 3: "До" — расчёт суммы

const prices = [100, 200];
let sum = 0;
for (let i = 0; i < prices.length; i++) {
  sum += prices[i];
}
document.getElementById("total").innerText = "Сумма: " + sum;

Недостаток: логика и DOM смешаны.

Пример 3: "После" — сервис

function calculateSum(prices) {
  return prices.reduce((total, price) => total + price, 0);
}
function renderTotal(sum) {
  document.getElementById("total").innerText = `Сумма: ${sum}`;
}
renderTotal(calculateSum([100, 200]));

Функция расчёта выделена в сервис.

Пример 4: "До" — fetch в событии

<button id="btn">Загрузить</button>
<div id="user"></div>
<script>
  document.getElementById("btn").addEventListener("click", function () {
    fetch("https://jsonplaceholder.typicode.com/users/1")
      .then((r) => r.json())
      .then((user) => {
        document.getElementById("user").innerHTML = user.name;
      });
  });
</script>

Недостаток: асинхронный код и DOM в обработчике.

Пример 4: "После" — сервис + View

// Сервис: получает данные пользователя
async function getUser(id) {
  const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`);
  if (!res.ok) throw new Error("Ошибка запроса");
  return await res.json();
}

// View: безопасно отображает данные в контейнере
function renderUser(user, containerId) {
  document.getElementById(containerId).textContent = user.name;
}

// Универсальный обработчик для кнопок
async function handleClick(id, containerId) {
  try {
    const user = await getUser(id);
    renderUser(user, containerId);
  } catch (err) {
    console.error(err);
  }
}

// Пример привязки к кнопке
document
  .getElementById("btn")
  .addEventListener("click", () => handleClick(1, "user"));

Код становится чище и надёжнее.

Практика

Итоги и домашнее задание

Основные выводы:

  • SRP, View, шаблоны, сервисы и MVC разделяют fetch и DOM.
  • Переиспользование: общие функции для разных мест.

Домашнее задание: Смотрите на портале

Дополнительные материалы

  • SRP и SOLID в JavaScript
  • MVC без фреймворков
  • Шаблоны в JavaScript

Вопросы?