2025-12-20 02:47:45 +03:00
2025-12-20 03:00:33 +03:00
2025-12-20 03:01:29 +03:00

Разрабатываем frontend-приложение и соединяем его с backend

Для секции "Веб-разработка: пример Fullstack"

Введение

На прошлом занятии мы поработали с Node.js и создали первое Web API. Мы реализовали API калькулятора и заметок, в процессе поработав с HTTP запросами. Теперь нам необходимо попытаться связать API фронтенда с API бекенда.

Для данной работы создан отдельный репозиторий по ссылке: https://git.weirdcat.su/ddi03/notes_app. Это очень простое приложение для управления заметками. Здесь уже реализована backend-часть и основа frontend-сервера. Мы рассмотрим, как они устроены, после чего реализуем требуемые страницы и их логику на JavaScript, чтобы можно было делать запросы на backend для подтягивания данных с помощью Fetch API.

Предполагается, что вы уже знакомы с HTML, CSS, изучили базовую работу с Git и помните, как работает простейшее API на Node.js.


📘 Урок 1: Загрузка проекта и подготовка к работе

Цель урока: Загрузить из удаленного репозитория изначальные файлы, подготовиться к работе с проектом и ознакомиться с работой серверов frontend и backend

Практические шаги:

  1. Перейдите в папку, куда вы хотите поместить проект из удаленного репозитория. Скачать репозиторий можно с помощью команды:
git clone https://git.weirdcat.su/ddi03/notes_app
  1. Перейдите в папку с приложением notes_app. В ней вы можете видеть уже созданную файловую структуру.
  2. Создайте ветку develop и перейдите в нее.
  3. Выполните npm install в папках backend и frontend, т.к. в свежем репозитории еще не установлены требуемые зависимости.

Обратите внимание, что у нас имеются папки для frontend и для backend. Как мы помним из предыдущего урока, frontend и backend в клиент-серверной архитектуре функционируют как два разных приложения. Несмотря на то, что backend и frontend разделены, мы решили использовать один репозиторий, хотя можно было бы использовать второй.

├── backend
│   ├── package.json
│   ├── package-lock.json
│   └── src
│       ├── app.js
│       ├── server.js
│       └── swagger.js
└── frontend
    ├── package.json
    ├── package-lock.json
    └── src
        ├── public
        │   ├── css
        │   │   ├── normalize.css
        │   │   ├── status.css
        │   │   └── style.css
        │   ├── html
        │   │   ├── 404.html
        │   │   ├── 500.html
        │   │   ├── index.html
        │   │   └── status.html
        │   ├── images
        │   │   └── logo.svg
        │   └── js
        │       └── helloWorld.js
        └── server.js

Наши backend и frontend оба написаны на Node.js. Задача backend-сервера - обрабатывать бизнес-логику. Рассмотрим, что делает сервер frontend.

Задача frontend-сервера - выдавать пользователю статические файлы по запросу. Статическими файлами называют те файлы, которые хранятся на фронтенд-сервере и не изменяются. Это страницы HTML, CSS, скрипты JS, картинки, файлы, шрифты и так далее. Фронтенд-сервер необходим, т.к. без него пользователю неоткуда получить файлы страниц и их наполнение.

└── frontend
    ├── package.json
    ├── package-lock.json
    └── src
        ├── public
        │   ├── css
        │   │   ├── normalize.css
        │   │   ├── status.css
        │   │   └── style.css
        │   ├── html
        │   │   ├── 404.html
        │   │   ├── 500.html
        │   │   ├── index.html
        │   │   └── status.html
        │   ├── images
        │   │   └── logo.svg
        │   └── js
        │       └── helloWorld.js
        └── server.js

В файле server.js у нас имеется блок кода, в котором мы говорим frontend-серверу, из какой папки нужно сервировать статические файлы для пользователя. для нашего приложения express создаем обработчик express.static, который по запросу пользователя выдает любой файл из соседней папки public:

import { dirname } from 'path';
import { fileURLToPath } from 'url';

const app = express();

const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename);

// Берем путь до текущей папки .../backend/src/public
app.use(express.static(join(__dirname, 'public')));

Теперь если пользователь откроет в браузере на нашем сайте URL /css/normalize.css, то сможет получить находящися в папке public соответствующий файл. Это работает аналогично и для всех других файлов: public/html/status.html, public/images/logo.svg и так далее.

Нам также важно, чтобы пользователь при переходе на главную страницу сайта по адресу / получал по умолчанию файл public/html/index.html, и для этого мы пишем ниже обработчик:

// Запросы на / выдают index.html по умолчанию
app.get('/', function(_, res) {
  res.sendFile(join(__dirname, 'public', 'html', 'index.html'));
});

Кроме того, мы хотим, чтобы пользователь мог перейти на любую страницу просто по названию в URL. Для этого мы пишем еще один обработчик:

// По всем остальным запросам пытаемся найти .html файл с названием,
// которое указал пользователь
app.get('/:page', function(req, res, next) {
  const page = req.params.page;
  const filePath = join(__dirname, 'public', 'html', `${page}.html`);
  
  // Проверяем существует ли файл
  fs.access(filePath, fs.constants.F_OK, (err) => {
    if (err) {
      // Файл не существует, передаем дальше
      next();
      return;
    }
    // Файл существует, отправляем его
    res.sendFile(filePath);
  });
});

Если пользователь откроет страницу /about или /status, то автоматически получит файл .../backend/src/html/about.html или ...backend/src/html/status.html, и это будет работать и для новых страниц в этой папке. Обратите внимание, что мы также зайдействуем переменную с функцией next: если запрашиваемая страница не найдется, мы предположим, что дальше в скрипте сервера найдется какой-то другой обработчик, который сможет справиться с запросом. Выполняя next() мы передаем обработку в следующие обработчики, которые могут проигнорировать или принять этот запрос.

Ниже у нас также есть обработчик, который выдает пользователю ошибку 404. Сюда мы как раз-то и попадаем, если в прошлом обработчике ничего не нашлось:

// Обработчик ошибки 404 - должен быть ПОСЛЕ всех маршрутов
// если ни один из предыдущих маршрутов не сработал - нам сюда.
// Отображает кастомную страничку об ошибке.
app.use(function(_, res) {
  res.status(404).sendFile(join(__dirname, 'public', 'html', '404.html'));
});

Мы пропустили один важный блок, назначение которого наиболее неочевидно:

// Маршрут с конфигурацией.
// Здесь javascript фронтенда может узнать, по какому адресу
// можно найти API бекенда
app.get('/config.json', (_, res) => {
  res.json({
    backendUrl: BACKEND_URL,
    environment: process.env.NODE_ENV || 'development',
  });
});

Он будет использоваться в нашем JavaScript для того, чтобы узнать адрес бекенда. Дело в том, что оба приложения могут быть настроены по-разному и находиться в совершенно разных местах. Что делать, если мы писали код на компьютере, а затем нам нужно разместить приложение на купленном хостинге? Получается, что придется переписывать в каждом запросе адрес нашего бекенда.

Чтобы этого не делать, мы создаем отдельный обработчик, в котором любой скрипт может спросить, по какому адресу расположено актуальное backend-приложение. К этому обработчику можно сделать HTTP-запрос и получить из объекта json значение backendUrl - ссылку на сервер бекенда, а далее использовать ее в скрипте.

Сервер бекенда реализован по тем же принципам, что мы изучили на прошлом занятии. Используются GET, POST, PUT и DELETE маршруты для управления заметками, данные для простоты урока хранятся внутри массива let notes = [];. Однако есть одно новшество, которое нам потребовалось добавить в backend - настройку CORS:

// middlware CORS разрешает запросы с других доменов. Без этого браузер
// запретит фронтенду общаться с бекендом!
import cors from 'cors';

app.use(cors({
  origin: 'http://localhost:8080', // разрешаем только с этого домена
  methods: ['GET', 'POST', 'PUT', 'DELETE'], // разрешаем только эти методы
  allowedHeaders: ['Content-Type'] // разрешаем только эти заголовки
}));

Cross-Origin Resource Sharing (CORS) - это один из защитных механизмов браузера. У него одна простая задача: не дать скриптам на одном сайте получить доступ к данным пользователя на другом сайте. Допустим, что вы зашли на некоторую вредоносную страницу, где загруженный js-файл сделал запрос на сайт банка с вашими даными для списания всех средств. Без политики CORS у него бы получилось это сделать. Браузер сначала уточняет у требуемого сайта, разрешает ли он запросы с других адресов. Если адреса первого и второго сайта совпадают, то всё в порядке. Если адреса отличаются, то второй сайт должен явно сообщить, что он разрешает запросы с любого другого сайта, либо с каких-то конкретных. Это делается во благо безопасности пользователя. Поскольку у фронтенда и бекенда в нашем приложении отличаются порты (8080 для фронтенда и 3000 для бекенда), настройка CORS для нас всё также необходима, да и в любом случае станет таковой в будущем при внедрении сайта. Подробнее про CORS на Хабр

📘 Урок 2: Запуск frontend- и backend-приложений

Цель урока: Освоить запуск отдельных серверов для frontend и backend, а также проверить их взаимодействие через специальную страницу.

Краткая теория:

Frontend-сервер отвечает за доставку статических файлов пользователю, таких как HTML-страницы и скрипты, и работает независимо от backend. Backend-сервер обрабатывает запросы к API, хранит данные и отвечает на них. Запуск приложений отдельно позволяет разрабатывать и тестировать части по отдельности, что упрощает отладку.

Теория: Значение разделения приложений

Раздельный запуск frontend и backend — это основа масштабируемой архитектуры веб-приложений. В реальной разработке frontend может быть размещен на одном сервере (например, для быстрой доставки статического контента через CDN), а backend — на другом, более мощном, для обработки сложных вычислений или баз данных. Это позволяет командам разработчиков работать параллельно: дизайнеры и frontend-специалисты фокусируются на интерфейсе, не дожидаясь backend, а backend-инженеры тестируют API независимо. Аналогия из жизни: представьте ресторан, где на кухне (backend) готовят блюда, а в зале (frontend), официанты подают блюда клиентам. Если кухня не работает, официанты все еще могут показать меню, но без еды опыт будет неполным.

Такая структура предотвращает "монолитные" проблемы, когда сбой в одной части останавливает все приложение. В нашем случае frontend запускается на порту 8080, а backend — на 3000, что имитирует разные домены. Без правильного запуска взаимодействие невозможно: браузер не сможет получить данные с backend из-за политик безопасности. Типичная ошибка — попытка обратиться к не запущенному серверу, что приводит к ошибкам соединения. Для отладки полезно использовать инструменты вроде браузерной консоли разработчика.

Практические шаги:

  1. Перейдите в папку frontend/src и запустите сервер с помощью команды в VSCode/Git Bash:

    node server.js
    

    Сервер frontend запустится на порту 8080.

  2. Откройте в браузере адрес http://localhost:8080/status. Вы увидите страницу проверки статуса, где будет указано, что сервер недоступен (это нормально, так как backend еще не запущен).

  3. Теперь перейдите в папку backend/src и запустите сервер backend аналогично:

    node server.js
    

    Сервер backend запустится на порту 3000.

  4. Обновите страницу http://localhost:8080/status в браузере. Теперь статус должен измениться на "Сервер работает!", с указанием времени ответа. Если появляется ошибка, проверьте, запущены ли оба сервера, и убедитесь, что порты не заняты другими приложениями (в этом случае перезапустите серверы). Также проблемой может быть неправильно настроенный CORS.

Что должно получиться / Проверка результата:

  • В консоли IDE для frontend появится сообщение "Frontend server running on port 8080".
  • В консоли IDE для backend — "Backend server running on port 3000".
  • На странице /status статус изменится с " Сервер недоступен" на " Сервер работает!", с отображением времени ответа (например, "150 мс").
  • Если статус не меняется, проверьте браузерную консоль на ошибки (нажмите F12) и убедитесь, что backend запущен.

📘 Урок 3: Создание интерфейса заметок

Цель урока: Создать HTML-страницу для управления заметками.

Теория:

HTML-формы позволяют пользователю вводить данные. Элементы <input> и <textarea> собирают информацию, которую JavaScript отправляет на сервер. CSS стилизует внешний вид страницы. Мы создадим одну страницу с формой для добавления/редактирования и списком существующих заметок.

Практические шаги:

  1. Создайте notes.html в frontend/src/public/html/:
<!DOCTYPE html>
<html lang="ru">
<head>
    <meta charset="UTF-8">
    <title>Заметки</title>
    <link rel="stylesheet" href="../css/notes.css">
</head>
<body>
    <div class="container">
        <h1>Мои заметки</h1>
        
        <div class="form-section">
            <h2 id="form-title">Новая заметка</h2>
            <form id="note-form">
                <input type="hidden" id="note-id">
                <input type="text" id="title" placeholder="Заголовок" required>
                <textarea id="text" placeholder="Текст заметки" required></textarea>
                <input type="text" id="author" placeholder="Автор" required>
                <button type="submit" id="submit-btn">Сохранить</button>
                <button type="button" id="cancel-btn" style="display:none">Отмена</button>
            </form>
        </div>

        <div class="notes-section">
            <button id="refresh-btn">Обновить список</button>
            <div id="notes-list"></div>
        </div>
    </div>

    <div id="delete-modal" class="modal">
        <div class="modal-content">
            <p>Удалить эту заметку?</p>
            <div>
                <button id="confirm-delete">Удалить</button>
                <button id="cancel-delete">Отмена</button>
            </div>
        </div>
    </div>

    <script src="../js/notes.js"></script>
</body>
</html>
  1. Создайте notes.css в frontend/src/public/css/:
.container { max-width: 800px; margin: 0 auto; padding: 20px; }
.form-section { background: #f5f5f5; padding: 20px; border-radius: 5px; margin-bottom: 30px; }
#note-form input, #note-form textarea { width: 100%; padding: 10px; margin: 10px 0; border: 1px solid #ddd; }
#note-form textarea { height: 100px; resize: vertical; }
button { padding: 10px 20px; margin: 5px; cursor: pointer; border: 1px solid #ccc; background: white; }
button:hover { background: #f0f0f0; }
.notes-section { margin-top: 30px; }
.note-card { border: 1px solid #ddd; padding: 15px; margin: 10px 0; border-radius: 5px; background: white; }
.note-actions { margin-top: 10px; }
.modal { display: none; position: fixed; top: 0; left: 0; width: 100%; height: 100%; background: rgba(0,0,0,0.5); }
.modal-content { background: white; padding: 20px; margin: 100px auto; width: 300px; border-radius: 5px; }

Что должно получиться:

  • Страница по адресу http://localhost:8080/notes с формой и местом для списка заметок
  • Простой чистый интерфейс

📘 Урок 4: Основы Fetch API и Promise

Основная концепция: Синхронный и асинхронный код

JavaScript по своей природе является однопоточным языком, что означает, что он может выполнять только одну операцию за раз. Когда JavaScript сталкивается с операциями, которые могут занять время (такие как запросы к серверу, чтение файлов или таймеры), он не может просто остановиться и ждать их завершения, потому что это заблокирует весь интерфейс.

Для решения этой проблемы используются асинхронные операции и Promise (Обещания). Fetch API возвращает именно Promise.

Теория: Что такое Promise?

Promise (Обещание) - это объект в JavaScript, который представляет результат асинхронной операции (операции, которая выполняется не сразу). У Promise есть три возможных состояния:

  1. Pending (Ожидание): операция еще выполняется
  2. Fulfilled (Выполнено): операция завершилась успешно
  3. Rejected (Отклонено): операция завершилась с ошибкой

Как работает Fetch API?

Функция fetch() принимает URL в качестве параметра и возвращает Promise. Этот Promise "оборачивает" HTTP-запрос к серверу. Поскольку запрос к серверу может занимать время (от миллисекунд до нескольких секунд), мы не можем ждать его завершения синхронно.

Методы .then() и .catch()

Когда fetch() возвращает Promise, мы можем использовать два метода для работы с ним:

  1. .then(функция) - выполняется, когда Promise переходит в состояние "выполнено"
  2. .catch(функция) - выполняется, когда Promise переходит в состояние "отклонено"

Практика: Загрузка конфигурации и списка заметок

// frontend/src/public/js/notes.js

// Глобальные переменные для хранения состояния
var backendUrl = '';
var editingNoteId = null;
var noteToDelete = null;

// Функция для загрузки конфигурации
function loadConfig() {
    // fetch() отправляет GET-запрос на сервер по указанному URL
    // Возвращает Promise, который оборачивает HTTP-запрос
    fetch('/config.json')
        .then(function(response) {
            // Первый .then() получает Response объект
            // Этот объект содержит информацию об ответе сервера
            // Но тело ответа еще не прочитано
            
            // Метод .json() читает тело ответа и парсит его как JSON
            // Он тоже возвращает Promise, потому что чтение тела - асинхронная операция
            return response.json();
        })
        .then(function(config) {
            // Второй .then() получает уже распарсенные JSON данные
            backendUrl = config.backendUrl;
            console.log('Backend URL загружен:', backendUrl);
        })
        .catch(function(error) {
            // .catch() выполняется если любая из предыдущих операций завершится ошибкой
            // Например: сервер недоступен, JSON некорректный и т.д.
            console.error('Ошибка загрузки конфигурации:', error);
            backendUrl = 'http://localhost:3000'; // резервный адрес
        });
}

// Функция для загрузки всех заметок
function loadNotes() {
    // Проверяем, загружен ли URL бэкенда
    if (!backendUrl) {
        // Если не загружен, сначала загружаем конфигурацию
        // и только потом продолжаем загрузку заметок
        loadConfig();
        
        // После загрузки конфигурации вызываем loadNotes снова
        setTimeout(function() {
            loadNotes();
        }, 100);
        return;
    }
    
    // Отправляем GET-запрос к бэкенду для получения списка заметок
    fetch(backendUrl + '/notes')
        .then(function(response) {
            // Проверяем, успешен ли HTTP-статус ответа
            if (!response.ok) {
                throw new Error('Сервер вернул ошибку: ' + response.status);
            }
            return response.json();
        })
        .then(function(result) {
            // result содержит данные в формате {success: true, data: [...]}
            if (result.success) {
                displayNotes(result.data);
            } else {
                console.error('Ошибка в ответе сервера');
            }
        })
        .catch(function(error) {
            // Обрабатываем ошибки сети или парсинга JSON
            console.error('Ошибка загрузки заметок:', error);
            document.getElementById('notes-list').innerHTML = 
                '<p>Ошибка загрузки заметок. Проверьте подключение к серверу.</p>';
        });
}

// Функция для отображения заметок
function displayNotes(notes) {
    var notesList = document.getElementById('notes-list');
    notesList.innerHTML = '';
    
    if (notes.length === 0) {
        notesList.innerHTML = '<p>Заметок нет. Создайте первую заметку!</p>';
        return;
    }
    
    // Проходим по массиву заметок и создаем HTML для каждой
    for (var i = 0; i < notes.length; i++) {
        var note = notes[i];
        var noteElement = document.createElement('div');
        noteElement.className = 'note-card';
        
        // Экранируем HTML-символы для безопасности
        var safeTitle = escapeHtml(note.title);
        var safeText = escapeHtml(note.text);
        var safeAuthor = escapeHtml(note.author);
        
        // Создаем дату в читаемом формате
        var date = new Date(note.updatedAt);
        var formattedDate = date.toLocaleDateString();
        
        noteElement.innerHTML = `
            <h3>${safeTitle}</h3>
            <p>${safeText}</p>
            <div class="note-meta">
                <small>Автор: ${safeAuthor}</small>
                <small>Дата: ${formattedDate}</small>
            </div>
            <div class="note-actions">
                <button onclick="editNote(${note.id})">Редактировать</button>
                <button onclick="showDeleteModal(${note.id}, '${safeTitle}')">Удалить</button>
            </div>
        `;
        
        notesList.appendChild(noteElement);
    }
}

// Функция для экранирования HTML
function escapeHtml(text) {
    var div = document.createElement('div');
    div.textContent = text;
    return div.innerHTML;
}

// Инициализация при загрузке страницы
document.addEventListener('DOMContentLoaded', function() {
    // Загружаем конфигурацию при старте
    loadConfig();
    
    // Загружаем заметки при старте
    loadNotes();
    
    // Назначаем обработчик для кнопки обновления
    document.getElementById('refresh-btn').addEventListener('click', function() {
        loadNotes();
    });
});

// Функция для редактирования заметки (доступна глобально)
window.editNote = function(noteId) {
    // Отправляем GET-запрос для получения конкретной заметки
    fetch(backendUrl + '/notes/' + noteId)
        .then(function(response) {
            return response.json();
        })
        .then(function(result) {
            if (result.success) {
                var note = result.data;
                
                // Заполняем форму данными заметки
                document.getElementById('note-id').value = note.id;
                document.getElementById('title').value = note.title;
                document.getElementById('text').value = note.text;
                document.getElementById('author').value = note.author;
                document.getElementById('form-title').textContent = 'Редактировать заметку';
                document.getElementById('submit-btn').textContent = 'Обновить';
                document.getElementById('cancel-btn').style.display = 'inline-block';
                
                editingNoteId = note.id;
            }
        })
        .catch(function(error) {
            console.error('Ошибка загрузки заметки для редактирования:', error);
        });
};

📘 Урок 5: Отправка данных на сервер (POST и PUT)

Теория: HTTP-методы для отправки данных

  1. POST - используется для создания новых ресурсов
  2. PUT - используется для обновления существующих ресурсов
  3. DELETE - используется для удаления ресурсов (будет в уроке 6)

Как отправлять данные с помощью fetch()

Для отправки данных нужно передать объект конфигурации вторым параметром в fetch():

fetch(url, {
    method: 'POST', // или 'PUT', 'DELETE'
    headers: {
        'Content-Type': 'application/json' // говорим серверу, что отправляем JSON
    },
    body: JSON.stringify(data) // преобразуем объект в JSON-строку
})

Практика: Создание и обновление заметок

// Добавьте этот код в тот же файл notes.js

// Обработка отправки формы
document.getElementById('note-form').addEventListener('submit', function(e) {
    // Отменяем стандартное поведение формы (перезагрузку страницы)
    e.preventDefault();
    
    // Собираем данные из формы
    var noteData = {
        title: document.getElementById('title').value,
        text: document.getElementById('text').value,
        author: document.getElementById('author').value
    };
    
    // Определяем URL и метод в зависимости от того, редактируем мы или создаем
    var url;
    var method;
    
    if (editingNoteId) {
        // Редактирование существующей заметки
        url = backendUrl + '/notes/' + editingNoteId;
        method = 'PUT';
    } else {
        // Создание новой заметки
        url = backendUrl + '/notes';
        method = 'POST';
    }
    
    // Отправляем запрос на сервер
    fetch(url, {
        method: method,
        headers: {
            'Content-Type': 'application/json'
        },
        body: JSON.stringify(noteData)
    })
        .then(function(response) {
            return response.json();
        })
        .then(function(result) {
            if (result.success) {
                // Сбрасываем форму
                resetForm();
                // Загружаем обновленный список заметок
                loadNotes();
            } else {
                console.error('Сервер вернул ошибку:', result.message);
            }
        })
        .catch(function(error) {
            console.error('Ошибка отправки данных:', error);
        });
});

// Функция для сброса формы
function resetForm() {
    document.getElementById('note-form').reset();
    document.getElementById('note-id').value = '';
    document.getElementById('form-title').textContent = 'Новая заметка';
    document.getElementById('submit-btn').textContent = 'Сохранить';
    document.getElementById('cancel-btn').style.display = 'none';
    editingNoteId = null;
}

// Обработчик для кнопки отмены
document.getElementById('cancel-btn').addEventListener('click', function() {
    resetForm();
});

📘 Урок 6: Удаление данных и обработка ошибок

Теория: Модальные окна и подтверждение действий

Удаление данных - критическая операция, поэтому хорошей практикой является запрос подтверждения у пользователя. Мы реализуем это через модальное окно.

Практика: Удаление заметок с подтверждением

// Добавьте этот код в файл notes.js

// Функция для показа модального окна удаления
window.showDeleteModal = function(noteId, noteTitle) {
    noteToDelete = noteId;
    
    // Показываем пользователю, какую заметку он собирается удалить
    document.getElementById('note-to-delete').textContent = noteTitle;
    document.getElementById('delete-modal').style.display = 'block';
};

// Обработчик для кнопки подтверждения удаления
document.getElementById('confirm-delete').addEventListener('click', function() {
    if (!noteToDelete) {
        return;
    }
    
    // Отправляем DELETE-запрос
    fetch(backendUrl + '/notes/' + noteToDelete, {
        method: 'DELETE'
    })
        .then(function(response) {
            return response.json();
        })
        .then(function(result) {
            if (result.success) {
                // Перезагружаем список заметок
                loadNotes();
            }
        })
        .catch(function(error) {
            console.error('Ошибка удаления:', error);
        })
        .finally(function() {
            // finally() выполнится в любом случае - успех или ошибка
            // Скрываем модальное окно
            document.getElementById('delete-modal').style.display = 'none';
            noteToDelete = null;
        });
});

// Обработчик для кнопки отмены удаления
document.getElementById('cancel-delete').addEventListener('click', function() {
    document.getElementById('delete-modal').style.display = 'none';
    noteToDelete = null;
});

// Закрытие модального окна при клике вне его
window.addEventListener('click', function(e) {
    if (e.target === document.getElementById('delete-modal')) {
        document.getElementById('delete-modal').style.display = 'none';
        noteToDelete = null;
    }
});

🎯 Дополнительные задания (опционально)

  1. Добавьте отображение сообщений об ошибках пользователю (вместо только console.error можно использовать alert() либо popup окна с помощью html и css);
  2. Добавьте возможность отметки заметки как "важной". В бекенде добавьте для модели заметки поле important: true/false и возможность редактирования этого поля через существующий маршурт PUT. На фронтенде добавьте какой-либо индикатор на заметку (кнопку с сердечком) которая отправляет запрос на редактирование статуса important, а также окрашивается в зависимости от статуса заметки.

📊 Схема работы Fetch API

Шаг 1: Вызов fetch(url)
       |
       ↓
Шаг 2: JavaScript создает Promise и начинает HTTP-запрос
       │
       ├─ Если запрос успешен → Promise переходит в состояние Fulfilled
       │  (выполняется .then(response => response.json()))
       │
       └─ Если ошибка сети → Promise переходит в состояние Rejected
          (выполняется .catch(error => ...))
       │
       ↓
Шаг 3: Чтение тела ответа (асинхронно)
       │
       ├─ Если JSON валиден → следующий .then(data => ...)
       │
       └─ Если JSON невалиден → .catch(error => ...)

🔍 Что важно запомнить:

  1. fetch() всегда возвращает Promise - объект, представляющий будущий результат
  2. .then() обрабатывает успешное выполнение, можно цепочкой вызывать несколько .then()
  3. .catch() обрабатывает ошибки на любом этапе цепочки
  4. response.json() тоже возвращает Promise, потому что чтение ответа - асинхронно
  5. finally() выполняется в любом случае - и при успехе, и при ошибке

Тестирование работы:

  1. Запустите серверы frontend и backend с помощью команды node
  2. Откройте в браузере: http://localhost:8080/notes
  3. Проверьте в консоли браузера (F12 → Console), что загрузился backend URL
  4. Создайте заметку через форму
  5. Нажмите "Обновить список" - заметка должна появиться
  6. Попробуйте отредактировать заметку
  7. Проверьте удаление с подтверждением
Description
No description provided
Readme 2.5 MiB