diff --git a/lessons/lesson22/lesson.md b/lessons/lesson22/lesson.md index 00a9bec..b94aa07 100644 --- a/lessons/lesson22/lesson.md +++ b/lessons/lesson22/lesson.md @@ -407,4 +407,4 @@ alert(s(3)(4)(5)()); // 12 - [Debug Visualizer](https://marketplace.visualstudio.com/items?itemName=hediet.debug-visualizer) - [Visualize JavaScript code execution](http://www.pythontutor.com/javascript.html#mode=edit) - [JavaScript Visualizer (ES5)](https://ui.dev/javascript-visualizer/) -- [Code to graph](https://crubier.github.io/code-to-graph/) \ No newline at end of file +- [Code to graph](https://crubier.github.io/code-to-graph/) diff --git a/lessons/lesson23/lesson.md b/lessons/lesson23/lesson.md index fbaba91..fd4955f 100644 --- a/lessons/lesson23/lesson.md +++ b/lessons/lesson23/lesson.md @@ -1,4 +1,5 @@ # Lesson 23 + ## OTUS Javascript Basic ### Разделение логики и представления @@ -9,25 +10,26 @@ #### Цели занятия -* Разобрать, как разделение кода на составляющие помогает с переиспользованием кода и его поддержкой (на простых примерах с запросами и DOM). -* Узнать подходы: принцип единственной ответственности, представление, шаблонизация, сервисный слой, MVC и увидеть, как они выражаются в коде (используя то, что уже известно: функции, async/await, fetch, DOM, события). -* На практике: маленькие примеры и рефакторинг (без классов — только функции). +- Разобрать, как разделение кода на составляющие помогает с переиспользованием кода и его поддержкой (на простых примерах с запросами и DOM). +- Узнать подходы: принцип единственной ответственности, представление, шаблонизация, сервисный слой, MVC и увидеть, как они выражаются в коде (используя то, что уже известно: функции, async/await, fetch, DOM, события). +- На практике: маленькие примеры и рефакторинг (без классов — только функции). #### **Компетенции:** -* Применение метанавыков для обработки информации и принятия решений. -* Умение структурировать программы. + +- Применение метанавыков для обработки информации и принятия решений. +- Умение структурировать программы. ### План занятия -* Введение: Зачем разделять — как "сортировка" в коде -* Теория: Определения и подходы. -* Маленькие примеры с запросами и DOM: "до/после" -* Лайвкодинг: Плохой код → шаг за шагом в MVC -* Итоги +- Введение: Зачем разделять — как "сортировка" в коде +- Теория: Определения и подходы. +- Маленькие примеры с запросами и DOM: "до/после" +- Лайвкодинг: Плохой код → шаг за шагом в MVC +- Итоги @@ -35,7 +37,6 @@ - ### Почему разделение — это легко и выгодно? Вы уже знаете DOM, async/await и fetch, функции и объекты. Но в простых скриптах часто всё смешивается: fetch в обработчике клика + innerHTML там же. Результат — дубли, ошибки при изменении (сломал UI — сломал запрос). @@ -55,16 +56,17 @@ ### fetch в обработчике + DOM ```javascript -document.getElementById('btn').addEventListener('click', async () => { - const response = await fetch('https://jsonplaceholder.typicode.com/users/1'); +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 = `
${JSON.stringify(data)}
`; + document.getElementById("output").innerHTML = `
${JSON.stringify( + data + )}
`; }); ``` Плохо: данные и отображение смешаны, нет переиспользования, любая ошибка в fetch ломает UI - ### Представление (View) @@ -79,10 +81,14 @@ document.getElementById('btn').addEventListener('click', async () => { ```javascript function renderDataAndFetch() { - fetch('https://jsonplaceholder.typicode.com/users/1') - .then(res => res.json()) - .then(data => { - document.getElementById('output').innerHTML = `
${JSON.stringify(data, null, 2)}
`; + fetch("https://jsonplaceholder.typicode.com/users/1") + .then((res) => res.json()) + .then((data) => { + document.getElementById("output").innerHTML = `
${JSON.stringify(
+        data,
+        null,
+        2
+      )}
`; }); } renderDataAndFetch(); @@ -90,20 +96,19 @@ renderDataAndFetch(); Плохо: функция одновременно делает fetch и вставку в DOM, нарушается принцип единственной ответственности - ### Шаблонизация Определение: генерация HTML из заготовки и данных. -Пример: +Пример: ```js for (let i = 0; i < data.length; i++) { - html += '

' + data[i] + '

'; + html += "

" + data[i] + "

"; } ``` @@ -113,15 +118,15 @@ for (let i = 0; i < data.length; i++) { Функции для запросов и расчётов. -Пример: +Пример: ### сервисный fetch встроен в обработчик ```javascript -document.getElementById('btn').addEventListener('click', async () => { - const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); +document.getElementById("btn").addEventListener("click", async () => { + const res = await fetch("https://jsonplaceholder.typicode.com/users/1"); const data = await res.json(); console.log(data); }); @@ -129,7 +134,6 @@ document.getElementById('btn').addEventListener('click', async () => { Плохо: нет переиспользования, каждый обработчик дублирует fetch, тестировать сложно - ### Теория: MVC @@ -142,12 +146,11 @@ Controller — связывает (addEventListener → model → view). Преимущества: структура и масштабируемость. - ### Примеры: "До" и "После" -Используем fetch, addEventListener, innerHTML. +Используем fetch, addEventListener, innerHTML. Каждый пример: "плохой" (смешанный) и "хороший" (разделённый). @@ -159,11 +162,11 @@ Controller — связывает (addEventListener → model → view).
``` @@ -174,13 +177,17 @@ document.getElementById('btn').addEventListener('click', showName); ### Пример 1: "После" ```javascript -function getName() { return 'Алекс'; } +function getName() { + return "Алекс"; +} function renderName(name) { - document.getElementById('name').innerHTML = `Имя: ${name}`; + document.getElementById("name").innerHTML = `Имя: ${name}`; } -document.getElementById('btn').addEventListener('click', () => renderName(getName())); +document + .getElementById("btn") + .addEventListener("click", () => renderName(getName())); ``` Разделение: данные и представление отделены. @@ -192,21 +199,20 @@ document.getElementById('btn').addEventListener('click', () => renderName(getNam ### Пример 2: "До" — список имён ```javascript -const names = ['Маша', 'Петя']; -let html = '"; // Здесь тоже уязвимость: innerHTML вставляет HTML напрямую -document.getElementById('list').innerHTML = html; - +document.getElementById("list").innerHTML = html; ``` Недостаток: небезопасно. @@ -216,16 +222,16 @@ document.getElementById('list').innerHTML = html; ### Пример 2: "После" — шаблон ```javascript -const names = ['Маша', 'Петя']; +const names = ["Маша", "Петя"]; function renderListSafe(items, containerId) { const container = document.getElementById(containerId); // создаём новый ul - const ul = document.createElement('ul'); + const ul = document.createElement("ul"); - items.map(name => { - const li = document.createElement('li'); + items.map((name) => { + const li = document.createElement("li"); li.textContent = name; // безопасно ul.appendChild(li); }); @@ -234,16 +240,13 @@ function renderListSafe(items, containerId) { container.replaceChildren(ul); } -renderListSafe(names, 'list'); - - +renderListSafe(names, "list"); ``` безопаснее - ### Пример 3: "До" — расчёт суммы ```javascript @@ -252,12 +255,13 @@ let sum = 0; for (let i = 0; i < prices.length; i++) { sum += prices[i]; } -document.getElementById('total').innerText = 'Сумма: ' + sum; +document.getElementById("total").innerText = "Сумма: " + sum; ``` Недостаток: логика и DOM смешаны. + ### Пример 3: "После" — сервис ```javascript @@ -265,7 +269,7 @@ function calculateSum(prices) { return prices.reduce((total, price) => total + price, 0); } function renderTotal(sum) { - document.getElementById('total').innerText = `Сумма: ${sum}`; + document.getElementById("total").innerText = `Сумма: ${sum}`; } renderTotal(calculateSum([100, 200])); ``` @@ -280,26 +284,27 @@ renderTotal(calculateSum([100, 200]));
``` Недостаток: асинхронный код и DOM в обработчике. + ### Пример 4: "После" — сервис + View ```javascript // Сервис: получает данные пользователя async function getUser(id) { const res = await fetch(`https://jsonplaceholder.typicode.com/users/${id}`); - if (!res.ok) throw new Error('Ошибка запроса'); + if (!res.ok) throw new Error("Ошибка запроса"); return await res.json(); } @@ -319,14 +324,15 @@ async function handleClick(id, containerId) { } // Пример привязки к кнопке -document.getElementById('btn').addEventListener('click', () => handleClick(1, 'user')); +document + .getElementById("btn") + .addEventListener("click", () => handleClick(1, "user")); ``` Код становится чище и надёжнее. - ### Практика @@ -335,22 +341,20 @@ document.getElementById('btn').addEventListener('click', () => handleClick(1, 'u Основные выводы: -* SRP, View, шаблоны, сервисы и MVC разделяют fetch и DOM. -* Переиспользование: общие функции для разных мест. +- SRP, View, шаблоны, сервисы и MVC разделяют fetch и DOM. +- Переиспользование: общие функции для разных мест. -### Домашнее задание: Смотрите на портале - +### Домашнее задание: Смотрите на портале ### Дополнительные материалы -* SRP и SOLID в JavaScript -* MVC без фреймворков -* Шаблоны в JavaScript - +- SRP и SOLID в JavaScript +- MVC без фреймворков +- Шаблоны в JavaScript diff --git a/lessons/lesson24/lesson.md b/lessons/lesson24/lesson.md index ae23b01..6b4afa1 100644 --- a/lessons/lesson24/lesson.md +++ b/lessons/lesson24/lesson.md @@ -8,25 +8,25 @@ #### Цели занятия -* Узнать подходы к проектированию частей приложения, которые упрощают поддержку и развитие. -* Разобраться, почему важна низкая связанность и высокая связанность внутри модуля (cohesion), и как этого достичь. -* Научиться выделять чистые функции и выносить побочные эффекты. -* Освоить внедрение зависимостей (Dependency Injection) на функциях и модулях. +- Узнать подходы к проектированию частей приложения, которые упрощают поддержку и развитие. +- Разобраться, почему важна низкая связанность и высокая связанность внутри модуля (cohesion), и как этого достичь. +- Научиться выделять чистые функции и выносить побочные эффекты. +- Освоить внедрение зависимостей (Dependency Injection) на функциях и модулях. #### Компетенции -* Владение синтаксисом JavaScript (модули `import`/`export`). -* Применение метанавыков для обработки информации и принятия решений в разработке. -* Умение структурировать программы и проектировать API модулей. +- Владение синтаксисом JavaScript (модули `import`/`export`). +- Применение метанавыков для обработки информации и принятия решений в разработке. +- Умение структурировать программы и проектировать API модулей. #### Формат и результаты -* Конспект занятия с примерами. -* Длительность: 90 минут. +- Конспект занятия с примерами. +- Длительность: 90 минут. @@ -47,10 +47,10 @@ Модуль — это логически связанный кусок кода с чёткой зоной ответственности и внешним API (экспортами). Делим код, чтобы: -* уменьшить когнитивную нагрузку (проще понимать части); -* переиспользовать и тестировать; -* изолировать изменения (правка внутри модуля не ломает остальные); -* управлять зависимостями явно. +- уменьшить когнитивную нагрузку (проще понимать части); +- переиспользовать и тестировать; +- изолировать изменения (правка внутри модуля не ломает остальные); +- управлять зависимостями явно. @@ -58,16 +58,16 @@ ```js // app.js -const form = document.querySelector('#form'); -const list = document.querySelector('#list'); +const form = document.querySelector("#form"); +const list = document.querySelector("#list"); const items = []; -form.addEventListener('submit', (e) => { +form.addEventListener("submit", (e) => { e.preventDefault(); - const value = form.elements.namedItem('title').value; + const value = form.elements.namedItem("title").value; items.push(value); - list.innerHTML = items.map((x) => `
  • ${x}
  • `).join(''); + list.innerHTML = items.map((x) => `
  • ${x}
  • `).join(""); }); ``` @@ -81,25 +81,27 @@ form.addEventListener('submit', (e) => { ```js // math.js — экспортируем ИМЕНОВАННО -export function add(a, b) { return a + b; } +export function add(a, b) { + return a + b; +} // app.js — импортируем ИМЕНОВАННО -import { add } from './math.js'; +import { add } from "./math.js"; console.log(add(2, 3)); ``` - Разделим по ролям (после): +Разделим по ролям (после): ```js // view.js — модуль представления // export: функция renderList — отвечает только за отображение списка export function renderList(container, items) { container.replaceChildren( - Object.assign(document.createElement('ul'), { - innerHTML: items.map((x) => `
  • ${x}
  • `).join(''), - }), + Object.assign(document.createElement("ul"), { + innerHTML: items.map((x) => `
  • ${x}
  • `).join(""), + }) ); } ``` @@ -109,7 +111,9 @@ export function renderList(container, items) { ```js // model.js — модуль чистой логики (без DOM и побочных эффектов) // export: addItem — ЧИСТАЯ функция, возвращает новый массив -export function addItem(model, value) { return [...model, value]; } +export function addItem(model, value) { + return [...model, value]; +} ``` @@ -117,23 +121,23 @@ export function addItem(model, value) { return [...model, value]; } ```js // app.js — композиция модулей и работа с DOM-событиями // import: берём renderList из view.js и addItem из model.js -import { renderList } from './view.js'; -import { addItem } from './model.js'; +import { renderList } from "./view.js"; +import { addItem } from "./model.js"; // ссылки на DOM-элементы и локальное состояние -const form = document.querySelector('#form'); -const list = document.querySelector('#list'); +const form = document.querySelector("#form"); +const list = document.querySelector("#list"); let items = []; // первый рендер пустого списка renderList(list, items); // обработчик: читаем ввод → обновляем модель → перерисовываем -form.addEventListener('submit', (e) => { +form.addEventListener("submit", (e) => { e.preventDefault(); - const value = form.elements.namedItem('title').value; // читаем значение из формы - items = addItem(items, value); // чистая логика - renderList(list, items); // отображение + const value = form.elements.namedItem("title").value; // читаем значение из формы + items = addItem(items, value); // чистая логика + renderList(list, items); // отображение }); ``` @@ -161,11 +165,11 @@ export function renderName(container, text) { } // app.js — импортируем и «склеиваем» -import { fullName } from './logic.js'; -import { renderName } from './view.js'; +import { fullName } from "./logic.js"; +import { renderName } from "./view.js"; -const name = fullName('Иван', 'Иванов'); // чистая логика -renderName(document.getElementById('out'), name); // отображение +const name = fullName("Иван", "Иванов"); // чистая логика +renderName(document.getElementById("out"), name); // отображение ``` @@ -174,9 +178,9 @@ renderName(document.getElementById('out'), name); // отображение ```js function submitFormAndRenderAndTrack(form, container, analytics) { - const value = form.elements.namedItem('email').value; - analytics.track('submit', { value }); - fetch('https://jsonplaceholder.typicode.com/users/1') + const value = form.elements.namedItem("email").value; + analytics.track("submit", { value }); + fetch("https://jsonplaceholder.typicode.com/users/1") .then((r) => r.json()) .then((data) => { container.innerHTML = `

    ${data.name}

    `; @@ -193,14 +197,14 @@ function submitFormAndRenderAndTrack(form, container, analytics) { ```js // domain.js (чистая логика) export function getEmail(form) { - return form.elements.namedItem('email').value.trim(); + return form.elements.namedItem("email").value.trim(); } // api.js (эффект) // загрузка пользователя по id (GET) export async function loadUser(id, http) { const res = await http(`https://jsonplaceholder.typicode.com/users/${id}`); - if (!res.ok) throw new Error('Request failed'); + if (!res.ok) throw new Error("Request failed"); return res.json(); } @@ -210,15 +214,15 @@ export function renderStatus(container, status) { } // app.js (склейка) -import { getEmail } from './domain.js'; -import { loadUser } from './api.js'; -import { renderStatus } from './view.js'; +import { getEmail } from "./domain.js"; +import { loadUser } from "./api.js"; +import { renderStatus } from "./view.js"; const http = window.fetch.bind(window); // внедряем зависимость async function onSubmit(form, container, analytics) { const email = getEmail(form); - analytics.track('submit', { email }); + analytics.track("submit", { email }); const user = await loadUser(1, http); // загрузка пользователя renderStatus(container, user.name); } @@ -230,29 +234,35 @@ async function onSubmit(form, container, analytics) { Чистая функция: -* зависит только от входных параметров; -* не меняет внешнее состояние (нет побочных эффектов); -* при одинаковом входе — одинаковый выход. +- зависит только от входных параметров; +- не меняет внешнее состояние (нет побочных эффектов); +- при одинаковом входе — одинаковый выход. Примеры чистых функций: ```js -function sum(a, b) { return a + b; } +function sum(a, b) { + return a + b; +} -function addItem(items, v) { return [...items, v]; } +function addItem(items, v) { + return [...items, v]; +} -function formatName(user) { return `${user.last} ${user.first}`.trim(); } +function formatName(user) { + return `${user.last} ${user.first}`.trim(); +} ``` Побочные эффекты (IO): -* DOM-операции (`innerHTML`, `addEventListener`), -* сеть (`fetch`, `WebSocket`), -* логирование (`console.log`). +- DOM-операции (`innerHTML`, `addEventListener`), +- сеть (`fetch`, `WebSocket`), +- логирование (`console.log`). Подход: вынести эффекты на «края» приложения, оставить «ядро» чистым. @@ -264,12 +274,14 @@ function formatName(user) { return `${user.last} ${user.first}`.trim(); } // до — смешение расчёта и эффекта (лог) function addAndLog(items, v) { const next = [...items, v]; - console.log('Updated:', next); + console.log("Updated:", next); return next; } // после — чистое ядро + эффект отдельно -function add(items, v) { return [...items, v]; } // чистая +function add(items, v) { + return [...items, v]; +} // чистая function logItems(prefix, items) { console.log(prefix, items); @@ -305,9 +317,12 @@ export function formatCurrency(value) { ```js // app.js — используем связный модуль как единое целое -import { subtotal, applyDiscount, formatCurrency } from './price.js'; +import { subtotal, applyDiscount, formatCurrency } from "./price.js"; -const items = [{ price: 100, qty: 2 }, { price: 50, qty: 1 }]; +const items = [ + { price: 100, qty: 2 }, + { price: 50, qty: 1 }, +]; const sum = subtotal(items); const discounted = applyDiscount(sum, 10); console.log(formatCurrency(discounted)); @@ -321,7 +336,7 @@ console.log(formatCurrency(discounted)); // api/users.js — функция зависит только от интерфейса http(url, init) export async function getUser(http, id) { const res = await http(`https://jsonplaceholder.typicode.com/users/${id}`); - if (!res.ok) throw new Error('HTTP error'); + if (!res.ok) throw new Error("HTTP error"); return res.json(); } @@ -355,7 +370,7 @@ loadProfile(http, 1); В тесте можно подменить `http` стабом: ```js -const fakeHttp = async () => ({ json: async () => ({ id: 1, name: 'Test' }) }); +const fakeHttp = async () => ({ json: async () => ({ id: 1, name: "Test" }) }); loadProfile(fakeHttp, 1).then((p) => console.log(p)); ``` @@ -368,14 +383,22 @@ DI на модуле (фабрика): export function makeCounter(start = 0) { let value = start; return { - inc() { value += 1; return value; }, - dec() { value -= 1; return value; }, - get() { return value; }, + inc() { + value += 1; + return value; + }, + dec() { + value -= 1; + return value; + }, + get() { + return value; + }, }; } // app.js -import { makeCounter } from './counter.js'; +import { makeCounter } from "./counter.js"; const counter = makeCounter(0); counter.inc(); ``` @@ -388,13 +411,13 @@ counter.inc(); ```js // запись строки -localStorage.setItem('greeting', 'hello'); +localStorage.setItem("greeting", "hello"); // чтение строки -const text = localStorage.getItem('greeting'); // 'hello' или null +const text = localStorage.getItem("greeting"); // 'hello' или null // удаление -localStorage.removeItem('greeting'); +localStorage.removeItem("greeting"); ``` @@ -403,11 +426,11 @@ localStorage.removeItem('greeting'); ```js // сохраняем объект — сначала сериализуем -const user = { id: 1, name: 'Alex' }; -localStorage.setItem('user', JSON.stringify(user)); +const user = { id: 1, name: "Alex" }; +localStorage.setItem("user", JSON.stringify(user)); // читаем и парсим -const raw = localStorage.getItem('user'); +const raw = localStorage.getItem("user"); const parsed = raw ? JSON.parse(raw) : null; ``` @@ -423,8 +446,11 @@ export function saveJSON(key, value) { export function loadJSON(key, fallback = null) { const raw = localStorage.getItem(key); - try { return raw ? JSON.parse(raw) : fallback; } - catch { return fallback; } + try { + return raw ? JSON.parse(raw) : fallback; + } catch { + return fallback; + } } ``` @@ -437,10 +463,14 @@ export function loadJSON(key, fallback = null) { ```js // model.js — чистые функции для списка (без id) // addTodo — не меняет исходный массив, возвращает новый -export function addTodo(list, text) { return [...list, text]; } +export function addTodo(list, text) { + return [...list, text]; +} // clearTodos — возвращает новый пустой список -export function clearTodos() { return []; } +export function clearTodos() { + return []; +} ``` @@ -448,15 +478,21 @@ export function clearTodos() { return []; } ```js // storage.js — обёртки над localStorage // ключ, под которым храним список задач -const KEY = 'todos'; +const KEY = "todos"; // save — сохраняет список как JSON-строку -export function save(list) { localStorage.setItem(KEY, JSON.stringify(list)); } +export function save(list) { + localStorage.setItem(KEY, JSON.stringify(list)); +} // load — читает список и парсит JSON, при ошибке вернёт [] export function load() { const raw = localStorage.getItem(KEY); - try { return raw ? JSON.parse(raw) : []; } catch { return []; } + try { + return raw ? JSON.parse(raw) : []; + } catch { + return []; + } } ``` @@ -469,7 +505,6 @@ export function load() { - @@ -483,13 +518,15 @@ export function load() { // простая отрисовка элементов списка в виде
  • // export: функция, которая принимает контейнер и массив строк export function renderList(container, list) { - container.innerHTML = list.map((t) => `
  • ${t}
  • `).join(''); + container.innerHTML = list.map((t) => `
  • ${t}
  • `).join(""); } // renderListWithIndex — отрисовка списка с индексами // export: функция, которая принимает контейнер и массив строк export function renderListWithIndex(container, list) { - container.innerHTML = list.map((t, i) => `
  • ${t}
  • `).join(''); + container.innerHTML = list + .map((t, i) => `
  • ${t}
  • `) + .join(""); } ``` @@ -498,33 +535,33 @@ export function renderListWithIndex(container, list) { ```js // app.js — склейка // import: берём чистые функции модели, обёртки для storage и функцию отображения -import { addTodo, clearTodos } from './model.js'; -import { save, load } from './storage.js'; -import { renderList } from './view.js'; +import { addTodo, clearTodos } from "./model.js"; +import { save, load } from "./storage.js"; +import { renderList } from "./view.js"; // ссылки на элементы интерфейса -const form = document.querySelector('#todo-form'); -const input = document.querySelector('#todo-input'); -const listEl = document.querySelector('#todo-list'); -const clearBtn = document.querySelector('#todo-clear'); +const form = document.querySelector("#todo-form"); +const input = document.querySelector("#todo-input"); +const listEl = document.querySelector("#todo-list"); +const clearBtn = document.querySelector("#todo-clear"); // инициализация состояния из localStorage let todos = load(); renderList(listEl, todos); // обработка добавления: берём текст → новая версия списка → сохраняем → перерисовываем -form.addEventListener('submit', (e) => { +form.addEventListener("submit", (e) => { e.preventDefault(); const text = input.value.trim(); if (!text) return; todos = addTodo(todos, text); save(todos); renderList(listEl, todos); - input.value = ''; + input.value = ""; }); // очистка списка по кнопке -clearBtn.addEventListener('click', () => { +clearBtn.addEventListener("click", () => { todos = clearTodos(); save(todos); renderList(listEl, todos); @@ -551,7 +588,9 @@ const addWithNow = makeAddWithNow(() => Date.now()); ```js // Дополнительно: удаление последнего элемента // model.popLast — возвращает новый список без последнего элемента -export function popLast(list) { return list.slice(0, -1); } +export function popLast(list) { + return list.slice(0, -1); +} // app.js — добавляем обработчик для кнопки #todo-pop // const popBtn = document.querySelector('#todo-pop'); @@ -573,7 +612,9 @@ export function removeAt(list, index) { // view.js — добавляем data-index для каждого
  • (пример альтернативной отрисовки) export function renderListWithIndex(container, list) { - container.innerHTML = list.map((t, i) => `
  • ${t}
  • `).join(''); + container.innerHTML = list + .map((t, i) => `
  • ${t}
  • `) + .join(""); } // app.js — делегирование событий: удаляем пункт, по которому кликнули @@ -586,6 +627,7 @@ export function renderListWithIndex(container, list) { // renderListWithIndex(listEl, todos); // }); ``` + ### 7. Мини-рефакторинг: «до/после» @@ -595,7 +637,7 @@ export function renderListWithIndex(container, list) { ```js async function onClick(btn, container) { btn.disabled = true; - const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); + const res = await fetch("https://jsonplaceholder.typicode.com/users/1"); const user = await res.json(); container.innerHTML = `

    ${user.name}

    `; btn.disabled = false; @@ -627,13 +669,17 @@ export function makeShowUserFlow({ http, toVM, render }) { } // app.js -import { makeShowUserFlow } from './app/flows.js'; -import { toUserViewModel } from './domain/userViewModel.js'; -import { renderUser } from './ui/render.js'; +import { makeShowUserFlow } from "./app/flows.js"; +import { toUserViewModel } from "./domain/userViewModel.js"; +import { renderUser } from "./ui/render.js"; const http = (url, init) => fetch(url, init); -const showUser = makeShowUserFlow({ http, toVM: toUserViewModel, render: renderUser }); +const showUser = makeShowUserFlow({ + http, + toVM: toUserViewModel, + render: renderUser, +}); ``` Результат: проще тестировать и менять отдельно представление и сеть. @@ -642,15 +688,17 @@ const showUser = makeShowUserFlow({ http, toVM: toUserViewModel, render: renderU ### 8. Частые анти-паттерны и как их исправить - #### (плохо) → SRP и разбиение (хорошо) ```js // плохо — функция делает и валидацию, и запрос, и рендер async function handle(form, container) { - const email = form.elements.namedItem('email').value; - if (!email.includes('@')) { container.textContent = 'bad email'; return; } - const res = await fetch('https://jsonplaceholder.typicode.com/users/1'); + const email = form.elements.namedItem("email").value; + if (!email.includes("@")) { + container.textContent = "bad email"; + return; + } + const res = await fetch("https://jsonplaceholder.typicode.com/users/1"); const data = await res.json(); container.textContent = data.name; } @@ -658,13 +706,24 @@ async function handle(form, container) { ```js // хорошо — разделение обязанностей -function validateEmail(email) { return email.includes('@'); } -function renderStatus(container, text) { container.textContent = text; } -async function loadUser(http, id) { return (await http(`https://jsonplaceholder.typicode.com/users/${id}`)).json(); } +function validateEmail(email) { + return email.includes("@"); +} +function renderStatus(container, text) { + container.textContent = text; +} +async function loadUser(http, id) { + return ( + await http(`https://jsonplaceholder.typicode.com/users/${id}`) + ).json(); +} async function onSubmit(http, form, container) { - const email = form.elements.namedItem('email').value; - if (!validateEmail(email)) { renderStatus(container, 'bad email'); return; } + const email = form.elements.namedItem("email").value; + if (!validateEmail(email)) { + renderStatus(container, "bad email"); + return; + } const data = await loadUser(http, 1); renderStatus(container, data.name); } @@ -695,7 +754,9 @@ function addTag(user, tag) { ```js // плохо -function isExpired(expAt) { return Date.now() > expAt; } +function isExpired(expAt) { + return Date.now() > expAt; +} // хорошо (DI времени) function makeIsExpired(now) { @@ -716,10 +777,10 @@ const isExpired = makeIsExpired(() => Date.now()); Основные выводы: -* SRP упрощает сопровождение: одна ответственность — одна причина менять код. -* Чистые функции — ядро, эффекты — по краям. -* Низкая связанность достигается через явные интерфейсы и DI. -* Модули `import/export` помогают структурировать код и API. +- SRP упрощает сопровождение: одна ответственность — одна причина менять код. +- Чистые функции — ядро, эффекты — по краям. +- Низкая связанность достигается через явные интерфейсы и DI. +- Модули `import/export` помогают структурировать код и API. @@ -732,10 +793,10 @@ const isExpired = makeIsExpired(() => Date.now()); ### Дополнительные материалы -* MDN: Modules — `import`/`export`. -* Статья: Принцип единственной ответственности (SRP). -* Pure functions and side effects (FP intro). -* Внедрение зависимостей без фреймворков (на функциях и фабриках). +- MDN: Modules — `import`/`export`. +- Статья: Принцип единственной ответственности (SRP). +- Pure functions and side effects (FP intro). +- Внедрение зависимостей без фреймворков (на функциях и фабриках). diff --git a/package.json b/package.json index 462b8e7..47b803b 100644 --- a/package.json +++ b/package.json @@ -7,9 +7,12 @@ "test": "echo \"Error: no test specified\" && exit 1", "dev": "reveal-md", "lint:prettier": "prettier --check .", + "lint:prettier:fix": "prettier --write .", "lint:spell": "yaspeller .", "lint": "npm run lint:prettier && npm run lint:code", + "lint:fix": "npm run lint:prettier:fix && npm run lint:code:fix", "lint:code": "eslint --ext .js,.ts,.md .", + "lint:code:fix": "eslint --ext .js,.ts,.md . --fix", "prepare": "husky install" }, "keywords": [],