
# Разрабатываем 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-формы позволяют пользователю вводить данные. Элементы `` и `