initial commit
This commit is contained in:
685
README.md
Normal file
685
README.md
Normal file
@@ -0,0 +1,685 @@
|
|||||||
|
# Разрабатываем 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), официанты подают блюда клиентам. Если кухня не работает, официанты все еще могут показать меню, но без еды опыт будет неполным.
|
||||||
|

|
||||||
|
|
||||||
|
Такая структура предотвращает "монолитные" проблемы, когда сбой в одной части останавливает все приложение. В нашем случае 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. Проверьте удаление с подтверждением
|
||||||
BIN
memes/fish.png
Normal file
BIN
memes/fish.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 2.3 MiB |
Reference in New Issue
Block a user