Files
lesson_3/README.md
2025-12-20 03:00:33 +03:00

687 lines
42 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
![](cover.avif)
# Разрабатываем frontend-приложение и соединяем его с backend
Для секции "Веб-разработка: пример Fullstack"
## Введение
На прошлом занятии мы поработали с Node.js и создали первое Web API. Мы реализовали API калькулятора и заметок, в процессе поработав с HTTP запросами. Теперь нам необходимо попытаться связать API фронтенда с API бекенда.
Для данной работы создан отдельный репозиторий по ссылке: https://git.weirdcat.su/ddi03/notes_app. Это очень простое приложение для управления заметками. Здесь уже реализована backend-часть и основа frontend-сервера. Мы рассмотрим, как они устроены, после чего реализуем требуемые страницы и их логику на JavaScript, чтобы можно было делать запросы на backend для подтягивания данных.
Предполагается, что вы уже знакомы с HTML, CSS, изучили базовую работу с Git и помните, как работает простейшее API на Node.js.
---
## 📘 **Урок 1: Загрузка проекта и подготовка к работе**
### **Цель урока**: Загрузить из удаленного репозитория изначальные файлы, подготовиться к работе с проектом и ознакомиться с работой серверов frontend и backend
### **Практические шаги**:
1. Перейдите в папку, куда вы хотите поместить проект из удаленного репозитория. Скачать репозиторий можно с помощью команды:
```bash
git clone https://git.weirdcat.su/ddi03/notes_app
```
2. Перейдите в папку с приложением `notes_app`. В ней вы можете видеть уже созданную файловую структуру.
3. Создайте ветку `develop` и перейдите в нее.
4. Выполните `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`:
```js
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`, и для этого мы пишем ниже обработчик:
```js
// Запросы на / выдают index.html по умолчанию
app.get('/', function(_, res) {
res.sendFile(join(__dirname, 'public', 'html', 'index.html'));
});
```
Кроме того, мы хотим, чтобы пользователь мог перейти на любую страницу просто по названию в URL. Для этого мы пишем еще один обработчик:
```js
// По всем остальным запросам пытаемся найти .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. Сюда мы как раз-то и попадаем, если в прошлом обработчике ничего не нашлось:
```js
// Обработчик ошибки 404 - должен быть ПОСЛЕ всех маршрутов
// если ни один из предыдущих маршрутов не сработал - нам сюда.
// Отображает кастомную страничку об ошибке.
app.use(function(_, res) {
res.status(404).sendFile(join(__dirname, 'public', 'html', '404.html'));
});
```
Мы пропустили один важный блок, назначение которого наиболее неочевидно:
```js
// Маршрут с конфигурацией.
// Здесь 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:
```js
// 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 на Хабр](https://habr.com/ru/companies/macloud/articles/553826/)
## 📘 **Урок 2: Запуск frontend- и backend-приложений**
### **Цель урока**: Освоить запуск отдельных серверов для frontend и backend, а также проверить их взаимодействие через специальную страницу.
### **Краткая теория**:
Frontend-сервер отвечает за доставку статических файлов пользователю, таких как HTML-страницы и скрипты, и работает независимо от backend. Backend-сервер обрабатывает запросы к API, хранит данные и отвечает на них. Запуск приложений отдельно позволяет разрабатывать и тестировать части по отдельности, что упрощает отладку.
### **Теория: Значение разделения приложений**
Раздельный запуск frontend и backend — это основа масштабируемой архитектуры веб-приложений. В реальной разработке frontend может быть размещен на одном сервере (например, для быстрой доставки статического контента через CDN), а backend — на другом, более мощном, для обработки сложных вычислений или баз данных. Это позволяет командам разработчиков работать параллельно: дизайнеры и frontend-специалисты фокусируются на интерфейсе, не дожидаясь backend, а backend-инженеры тестируют API независимо. Аналогия из жизни: представьте ресторан, где на кухне (backend) готовят блюда, а в зале (frontend), официанты подают блюда клиентам. Если кухня не работает, официанты все еще могут показать меню, но без еды опыт будет неполным.
![](memes/fish.png)
Такая структура предотвращает "монолитные" проблемы, когда сбой в одной части останавливает все приложение. В нашем случае frontend запускается на порту 8080, а backend — на 3000, что имитирует разные домены. Без правильного запуска взаимодействие невозможно: браузер не сможет получить данные с backend из-за политик безопасности. Типичная ошибка — попытка обратиться к не запущенному серверу, что приводит к ошибкам соединения. Для отладки полезно использовать инструменты вроде браузерной консоли разработчика.
### **Практические шаги**:
1. Перейдите в папку `frontend/src` и запустите сервер с помощью команды в VSCode/Git Bash:
```bash
node server.js
```
Сервер frontend запустится на порту 8080.
2. Откройте в браузере адрес `http://localhost:8080/status`. Вы увидите страницу проверки статуса, где будет указано, что сервер недоступен (это нормально, так как backend еще не запущен).
3. Теперь перейдите в папку `backend/src` и запустите сервер backend аналогично:
```bash
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/`:
```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>
```
2. Создайте `notes.css` в `frontend/src/public/css/`:
```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 переходит в состояние "отклонено"
### **Практика: Загрузка конфигурации и списка заметок**
```javascript
// 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():
```javascript
fetch(url, {
method: 'POST', // или 'PUT', 'DELETE'
headers: {
'Content-Type': 'application/json' // говорим серверу, что отправляем JSON
},
body: JSON.stringify(data) // преобразуем объект в JSON-строку
})
```
### **Практика: Создание и обновление заметок**
```javascript
// Добавьте этот код в тот же файл 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: Удаление данных и обработка ошибок**
### **Теория: Модальные окна и подтверждение действий**
Удаление данных - критическая операция, поэтому хорошей практикой является запрос подтверждения у пользователя. Мы реализуем это через модальное окно.
### **Практика: Удаление заметок с подтверждением**
```javascript
// Добавьте этот код в файл 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. Проверьте удаление с подтверждением