Полный курс Full-Stack JavaScript (React,
[Link], Express, [Link])
Введение
Добро пожаловать в полный курс по Full-Stack JavaScript разработке! Этот курс
разработан специально для тех, кто уже имеет базовые знания JavaScript и стремится
стать продакшн-готовым специалистом. Мы погрузимся в мир современных
технологий, используя один из самых популярных и востребованных стеков: React
для фронтенда, [Link] и Express для бэкенда, и [Link] для оптимизации и серверного
рендеринга. Этот стек, часто называемый аналогом MERN (MongoDB, Express, React,
[Link]), позволит вам создавать мощные, масштабируемые и
высокопроизводительные веб-приложения.
Моя цель как вашего ментора — не просто перечислить темы, а дать глубокое
понимание каждой концепции, объяснить «почему» и «как», а также предоставить
достаточно практики, чтобы вы могли уверенно применять полученные знания в
реальных проектах. Каждая глава будет содержать четкие объяснения, практические
задания и мини-проекты, которые постепенно будут накапливаться в итоговый
полноценный проект. Особое внимание будет уделено качеству кода, архитектуре
приложений, масштабируемости, тестированию, DevOps и безопасности.
Приготовьтесь к интенсивному, но увлекательному путешествию в мир Full-Stack
разработки. Давайте начнем!
Глава 1: Углубление основ и среда разработки
В первой главе мы заложим прочный фундамент для всего курса. Даже если у вас есть
базовые знания JavaScript, мы быстро повторим и углубимся в современные
возможности языка (ES6+), которые критически важны для написания чистого,
эффективного и поддерживаемого кода. Мы разберем такие концепции, как области
видимости, замыкания, промисы и async/await , классы и модули. Понимание этих тем
позволит вам писать более предсказуемый и производительный код, а также
эффективно работать с асинхронными операциями, которые являются неотъемлемой
частью JavaScript-разработки, особенно на бэкенде.
Помимо языка, мы настроим вашу инструментальную среду. Правильно настроенное
окружение — это половина успеха. Мы установим [Link], который является движком
JavaScript вне браузера и основой для нашего бэкенда. Вы познакомитесь с npm
(Node Package Manager) и yarn — менеджерами пакетов, которые позволяют легко
управлять зависимостями в ваших проектах. Также мы освоим Git и GitHub —
системы контроля версий, без которых невозможно представить современную
командную разработку. Вы научитесь работать с ветками, коммитами и пулл-
реквестами, что является ключевым навыком для совместной работы над проектами.
Особое внимание уделим основам [Link]. Мы создадим простой HTTP-сервер на
[Link] без использования фреймворков. Это может показаться излишним, но такой
подход поможет вам глубоко понять event-loop и неблокирующую архитектуру
[Link], что является краеугольным камнем для построения
высокопроизводительных серверных приложений. Понимание event-loop позволит
вам писать более эффективный код и диагностировать проблемы
производительности. Мы также разберем модульную систему [Link] и работу с
событиями, что является основой для создания масштабируемых приложений.
Эта глава заложит базу для всего курса, обеспечивая вас необходимыми знаниями и
инструментами для дальнейшего изучения React, Express, [Link] и других
продвинутых тем. Давайте приступим к детальному изучению каждой темы.
1.1 Современные возможности JavaScript (ES6+)
JavaScript постоянно развивается, и с выходом стандарта ECMAScript 2015 (ES6) и
последующих версий (ES7, ES8 и т.д.) в язык было добавлено множество мощных
функций, которые значительно упрощают разработку и делают код более читаемым и
эффективным. Для Full-Stack разработчика крайне важно быть в курсе этих
изменений и активно их использовать.
1.1.1 Области видимости (Scope) и Замыкания (Closures)
Понимание областей видимости и замыканий является фундаментальным для
написания корректного и предсказуемого JavaScript-кода. В JavaScript существует три
основных типа областей видимости: глобальная, функциональная и блочная.
• Глобальная область видимости: Переменные, объявленные вне каких-либо
функций или блоков, доступны из любой точки программы.
• Функциональная область видимости: Переменные, объявленные внутри
функции с использованием var , доступны только внутри этой функции. Это
означает, что переменные, объявленные внутри функции, не видны извне.
• Блочная область видимости (ES6+): С появлением let и const в ES6, JavaScript
получил блочную область видимости. Это означает, что переменные,
объявленные с let или const внутри блока ( {} ), доступны только внутри этого
блока. Это значительно уменьшает вероятность ошибок, связанных с утечкой
переменных и их перезаписью.
Замыкания — это мощная концепция, тесно связанная с областями видимости.
Замыкание возникает, когда функция «запоминает» свою лексическую область
видимости (окружение, в котором она была объявлена), даже если она выполняется
вне этой области видимости. Это позволяет функциям получать доступ к переменным
из их родительской области видимости после того, как родительская функция
завершила свое выполнение. Замыкания часто используются для создания
приватных переменных, каррирования функций и сохранения состояния.
Пример замыкания:
JavaScript
function createCounter() {
let count = 0; // Эта переменная находится в лексической области видимости
createCounter
return function() {
count++;
return count;
};
}
const counter1 = createCounter();
[Link](counter1()); // 1
[Link](counter1()); // 2
const counter2 = createCounter();
[Link](counter2()); // 1 (у counter2 свой собственный 'count')
В этом примере внутренняя анонимная функция является замыканием. Она имеет
доступ к переменной count из внешней функции createCounter , даже после того, как
createCounter завершила свое выполнение. Каждое вызов createCounter() создает
новое замыкание с собственной переменной count .
1.1.2 Промисы (Promises) и async/await
Асинхронные операции являются неотъемлемой частью JavaScript, особенно при
работе с сетью (HTTP-запросы) или файловой системой. До появления промисов и
async/await асинхронный код часто приводил к «аду коллбэков» (callback hell), делая
код трудночитаемым и сложным для отладки.
Промисы представляют собой объекты, которые представляют конечное завершение
(или неудачу) асинхронной операции и ее результирующее значение. Промис может
находиться в одном из трех состояний:
• pending (ожидание): начальное состояние, промис еще не выполнен и не
отклонен.
• fulfilled (выполнено): операция успешно завершена, и промис имеет
результирующее значение.
• rejected (отклонено): операция завершилась с ошибкой, и промис имеет причину
отклонения.
Промисы позволяют цепочно вызывать асинхронные операции с помощью методов
.then() (для успешного выполнения) и .catch() (для обработки ошибок), что делает
код более линейным и понятным.
Пример промиса:
JavaScript
function fetchData() {
return new Promise((resolve, reject) => {
setTimeout(() => {
const success = true;
if (success) {
resolve('Данные успешно получены!');
} else {
reject('Ошибка при получении данных.');
}
}, 2000);
});
}
fetchData()
.then(data => {
[Link](data); // Данные успешно получены!
})
.catch(error => {
[Link](error); // Ошибка при получении данных.
});
async/await — это синтаксический сахар над промисами, который делает
асинхронный код еще более похожим на синхронный, что значительно улучшает его
читаемость и упрощает обработку ошибок. Функция, объявленная с ключевым
словом async , всегда возвращает промис. Внутри async функции можно
использовать ключевое слово await перед вызовом промиса. await «ставит на
паузу» выполнение async функции до тех пор, пока промис не будет выполнен или
отклонен, и возвращает его результат.
Пример async/await :
JavaScript
async function getData() {
try {
const data = await fetchData(); // fetchData возвращает промис
[Link](data);
} catch (error) {
[Link](error);
}
}
getData();
Использование async/await является предпочтительным способом работы с
асинхронностью в современном JavaScript, так как оно делает код более чистым и
легким для понимания, особенно при работе с несколькими последовательными
асинхронными операциями.
1.1.3 Классы (Classes) и Модули (Modules)
Классы в JavaScript (появившиеся в ES6) предоставляют синтаксический сахар для
создания объектов на основе прототипов. Они делают код более объектно-
ориентированным и привычным для разработчиков, пришедших из других языков,
таких как Java или C#. Классы упрощают создание конструкторов, методов и
наследования.
Пример класса:
JavaScript
class Person {
constructor(name, age) {
[Link] = name;
[Link] = age;
}
greet() {
return `Привет, меня зовут ${[Link]} и мне ${[Link]} лет.`;
}
}
class Student extends Person {
constructor(name, age, studentId) {
super(name, age); // Вызов конструктора родительского класса
[Link] = studentId;
}
study() {
return `${[Link]} учится с ID ${[Link]}.`;
}
}
const person1 = new Person('Алиса', 30);
[Link]([Link]()); // Привет, меня зовут Алиса и мне 30 лет.
const student1 = new Student('Боб', 20, 'S12345');
[Link]([Link]()); // Привет, меня зовут Боб и мне 20 лет.
[Link]([Link]()); // Боб учится с ID S12345.
Модули в JavaScript (также введенные в ES6) предоставляют стандартизированный
способ организации кода в отдельные, переиспользуемые файлы. Модули позволяют
инкапсулировать код, предотвращая загрязнение глобальной области видимости и
улучшая поддерживаемость проекта. Они используют ключевые слова import для
импорта функциональности из других модулей и export для экспорта
функциональности.
Пример использования модулей:
[Link] :
JavaScript
export function add(a, b) {
return a + b;
}
export const PI = 3.14159;
export default class Calculator {
add(a, b) {
return a + b;
}
}
[Link] :
JavaScript
import { add, PI } from './[Link]';
import Calculator from './[Link]'; // Импорт дефолтного экспорта
[Link](add(5, 3)); // 8
[Link](PI); // 3.14159
const calc = new Calculator();
[Link]([Link](10, 2)); // 12
Модули являются краеугольным камнем для построения больших и сложных
приложений, так как они способствуют разделению ответственности и улучшают
организацию кода. В [Link] и современных фронтенд-фреймворках (React, [Link])
модули используются повсеместно.
1.2 Окружение разработки ([Link], npm/yarn, Git/GitHub)
Прежде чем мы начнем писать код, нам необходимо настроить правильное
окружение разработки. Это включает установку [Link], менеджеров пакетов npm
или yarn , а также настройку системы контроля версий Git и работу с GitHub.
1.2.1 Установка [Link]
[Link] — это кроссплатформенная среда выполнения JavaScript, которая позволяет
выполнять JavaScript-код вне браузера. Она основана на движке V8 JavaScript от
Google Chrome и является основой для разработки серверных приложений,
инструментов командной строки и многого другого. [Link] поставляется со
встроенным менеджером пакетов npm .
Шаги по установке [Link]:
1. Скачайте установщик: Перейдите на официальный сайт [Link]:
[Link] Вы увидите две рекомендуемые версии: LTS (Long Term Support)
и Current (последняя версия с новыми функциями). Для большинства проектов
рекомендуется использовать LTS-версию, так как она более стабильна и
поддерживается дольше.
2. Запустите установщик: Следуйте инструкциям установщика. Обычно достаточно
нажимать «Далее» (Next) и соглашаться с условиями. Установщик автоматически
добавит [Link] и npm в ваш системный PATH.
3. Проверьте установку: Откройте терминал (командную строку) и выполните
следующие команды, чтобы убедиться, что [Link] и npm установлены корректно:
1.2.2 npm и yarn: Менеджеры пакетов
npm (Node Package Manager) — это менеджер пакетов по умолчанию для [Link]. Он
позволяет устанавливать, обновлять и удалять библиотеки и фреймворки, а также
управлять зависимостями вашего проекта. npm также является огромным
репозиторием (реестром) открытого исходного кода JavaScript-пакетов.
Основные команды npm:
• npm init : Инициализирует новый проект [Link] и создает файл [Link] .
Этот файл содержит метаданные о вашем проекте (название, версия, описание,
автор) и список его зависимостей.
• npm install <package-name> : Устанавливает пакет и добавляет его в dependencies в
[Link] . Зависимости, перечисленные в dependencies , необходимы для
работы вашего приложения в продакшене.
• npm install <package-name> --save-dev или npm install <package-name> -D : Устанавливает
пакет и добавляет его в devDependencies в [Link] . Зависимости,
перечисленные в devDependencies , необходимы только для разработки и
тестирования (например, линтеры, бандлеры, тестовые фреймворки).
• npm install : Устанавливает все зависимости, перечисленные в [Link] (как
dependencies , так и devDependencies ).
• npm uninstall <package-name> : Удаляет пакет.
• npm update <package-name> : Обновляет пакет до последней совместимой версии.
• npm start : Запускает скрипт, определенный в поле start в [Link] .
• npm run <script-name> : Запускает любой пользовательский скрипт, определенный
в поле scripts в [Link] .
yarn — это альтернативный менеджер пакетов, разработанный Facebook. Он был
создан для решения некоторых проблем с производительностью и надежностью,
которые существовали в npm в прошлом. Сегодня npm значительно улучшился, и
выбор между npm и yarn часто сводится к личным предпочтениям или требованиям
проекта. Оба менеджера пакетов выполняют схожие функции.
Основные команды yarn (аналогичные npm):
• yarn init : Инициализирует новый проект.
• yarn add <package-name> : Устанавливает пакет и добавляет его в dependencies .
• yarn add <package-name> --dev : Устанавливает пакет и добавляет его в
devDependencies .
• yarn : Устанавливает все зависимости.
• yarn remove <package-name> : Удаляет пакет.
• yarn upgrade <package-name> : Обновляет пакет.
• yarn start : Запускает скрипт start .
• yarn <script-name> : Запускает пользовательский скрипт.
В этом курсе мы будем использовать npm по умолчанию, но вы можете свободно
использовать yarn , если вам это удобнее.
1.2.3 Git и GitHub: Система контроля версий
Git — это распределенная система контроля версий, которая позволяет отслеживать
изменения в вашем коде, возвращаться к предыдущим версиям, работать над
проектом в команде и многое другое. Это абсолютно необходимый инструмент для
любого разработчика.
GitHub — это веб-сервис для хостинга репозиториев Git. Он предоставляет удобный
интерфейс для совместной работы, управления проектами, отслеживания ошибок и
проведения ревью кода. GitHub стал стандартом де-факто для хостинга открытого
исходного кода и частных проектов.
Шаги по установке Git:
1. Скачайте установщик: Перейдите на официальный сайт Git: [Link]
[Link]/downloads. Выберите установщик для вашей операционной системы.
2. Запустите установщик: Следуйте инструкциям. Для большинства пользователей
настройки по умолчанию будут достаточными.
3. Проверьте установку: Откройте терминал и выполните команду:
Настройка Git:
После установки Git необходимо настроить ваше имя пользователя и адрес
электронной почты. Эти данные будут использоваться для идентификации ваших
коммитов.
Bash
git config --global [Link] "Ваше Имя"
git config --global [Link] "ваша_почта@[Link]"
Основные команды Git:
• git init : Инициализирует новый репозиторий Git в текущей директории.
• git clone <repository-url> : Клонирует существующий репозиторий с удаленного
сервера (например, GitHub) на ваш локальный компьютер.
• git status : Показывает текущее состояние репозитория: какие файлы изменены,
какие готовы к коммиту, какие не отслеживаются.
• git add <file-name> или git add . : Добавляет изменения в файл(ы) в staging area
(область подготовленных изменений) для следующего коммита. git add .
добавляет все измененные и новые файлы.
• git commit -m "Ваше сообщение коммита" : Записывает подготовленные изменения в
историю репозитория. Сообщение коммита должно быть кратким и
информативным, описывающим, что было изменено.
• git log : Показывает историю коммитов.
• git branch : Показывает список веток в репозитории.
• git branch <new-branch-name> : Создает новую ветку.
• git checkout <branch-name> : Переключается на другую ветку.
• git merge <branch-name> : Объединяет изменения из указанной ветки в текущую
ветку.
• git pull : Загружает изменения с удаленного репозитория и объединяет их с вашей
текущей веткой.
• git push : Отправляет ваши локальные коммиты на удаленный репозиторий.
• git remote add origin <repository-url> : Добавляет удаленный репозиторий с именем
origin .
Рабочий процесс с Git и GitHub:
Типичный рабочий процесс выглядит так:
1. Создание репозитория: Создайте новый репозиторий на GitHub или
инициализируйте его локально и свяжите с GitHub.
2. Клонирование: Клонируйте репозиторий на свой локальный компьютер.
3. Создание ветки: Для каждой новой фичи или исправления ошибки создавайте
новую ветку ( git checkout -b feature/my-new-feature ).
4. Разработка: Вносите изменения в код.
5. Коммит: Регулярно сохраняйте свои изменения с помощью git add и git commit .
6. Публикация: Отправляйте свою ветку на GitHub ( git push origin feature/my-new-
feature ).
7. Пулл-реквест (Pull Request): Создайте пулл-реквест на GitHub, чтобы
предложить свои изменения для слияния с основной веткой (например, main
или master ). Это позволяет другим членам команды просмотреть ваш код и
оставить комментарии.
8. Ревью и слияние: После ревью и одобрения ваш пулл-реквест будет объединен с
основной веткой.
9. Обновление локальной ветки: После слияния обновите свою локальную
основную ветку ( git checkout main && git pull ).
Понимание и активное использование Git и GitHub является критически важным
навыком для любого современного разработчика, так как это основа для совместной
работы и управления версиями кода.
1.3 Основы [Link]
[Link] — это не просто среда выполнения JavaScript, это мощная платформа для
создания масштабируемых сетевых приложений. Ее ключевая особенность —
неблокирующая, управляемая событиями архитектура, которая делает ее идеальной
для работы с большим количеством одновременных соединений.
1.3.1 Event Loop и неблокирующий I/O
Сердцем [Link] является Event Loop (цикл событий). В отличие от традиционных
серверных языков, которые используют многопоточную модель (каждый запрос
обрабатывается отдельным потоком), [Link] работает в однопоточном режиме. Это
означает, что все операции выполняются в одном потоке. Но как же тогда [Link]
обрабатывает множество запросов одновременно, не блокируя выполнение?
Ответ кроется в неблокирующем I/O (вводе/выводе) и Event Loop. Когда [Link]
сталкивается с операцией, которая может занять много времени (например, чтение
файла, запрос к базе данных, HTTP-запрос к внешнему API), он не ждет ее
завершения. Вместо этого он передает эту операцию в системное ядро (которое
может использовать многопоточность или другие механизмы для выполнения I/O) и
продолжает выполнять следующий код. Как только долгая операция завершается,
она помещает коллбэк в очередь событий. Event Loop постоянно проверяет эту
очередь и, когда основной поток свободен, забирает коллбэки из очереди и
выполняет их.
Эта модель позволяет [Link] эффективно обрабатывать большое количество
одновременных соединений с минимальными накладными расходами, так как нет
необходимости создавать и управлять множеством потоков. Однако это также
означает, что любая «тяжелая» (CPU-bound) операция, которая выполняется
синхронно в основном потоке, будет блокировать Event Loop и замедлять все
приложение.
Фазы Event Loop (упрощенно):
1. timers: Выполняет коллбэки, запланированные setTimeout() и setInterval() .
2. pending callbacks: Выполняет коллбэки отложенных I/O операций.
3. idle, prepare: Только для внутреннего использования [Link].
4. poll: Извлекает новые I/O события; выполняет I/O-связанные коллбэки (почти все,
кроме таймеров и setImmediate ); в некоторых случаях блокируется здесь, ожидая
I/O.
5. check: Выполняет коллбэки setImmediate() .
6. close callbacks: Выполняет коллбэки закрытия (например, [Link]('close', ...) ).
Важно понимать, что [Link]() и промисы ( .then() , await ) имеют более
высокий приоритет и выполняются до того, как Event Loop перейдет к следующей
фазе.
1.3.2 Модульная система [Link]
[Link] использует модульную систему CommonJS (хотя современные версии также
поддерживают ES-модули). Это позволяет разбивать приложение на небольшие,
управляемые части, каждая из которых является отдельным модулем. Модули
инкапсулируют код, предотвращая конфликты имен и улучшая организацию проекта.
• require() : Используется для импорта модулей.
• [Link] / exports : Используется для экспорта функциональности из
модуля.
Пример модульной системы CommonJS:
[Link] :
JavaScript
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
[Link] = {
add: add,
subtract: subtract
};
[Link] :
JavaScript
const math = require('./math');
[Link]([Link](10, 5)); // 15
[Link]([Link](10, 5)); // 5
С появлением ES-модулей в [Link] (начиная с версии 12, с полной поддержкой в 14+)
вы также можете использовать синтаксис import / export из ES6, если ваш проект
настроен на это (например, через "type": "module" в [Link] или использование
расширения .mjs ).
1.3.3 Создание простого HTTP-сервера на [Link]
Чтобы лучше понять Event Loop и неблокирующий I/O, давайте создадим простой
HTTP-сервер, который будет отвечать на запросы. Мы будем использовать
встроенный модуль http .
JavaScript
// [Link]
const http = require('http');
const hostname = '[Link]'; // localhost
const port = 3000;
// Создаем сервер
const server = [Link]((req, res) => {
// req (request) - объект запроса, res (response) - объект ответа
// Устанавливаем заголовок ответа: Content-Type и статус
[Link] = 200;
[Link]('Content-Type', 'application/json');
// Определяем маршрут
if ([Link] === '/hello' && [Link] === 'GET') {
const responseData = {
message: 'Привет от простого [Link] сервера!',
timestamp: new Date().toISOString()
};
[Link]([Link](responseData));
} else if ([Link] === '/' && [Link] === 'GET') {
[Link]([Link]({ message: 'Добро пожаловать на главную страницу!'
}));
} else {
[Link] = 404;
[Link]([Link]({ error: 'Страница не найдена' }));
}
});
// Запускаем сервер
[Link](port, hostname, () => {
[Link](`Сервер запущен по адресу [Link]
[Link]('Попробуйте перейти по [Link]
});
Как это работает:
1. const http = require('http'); : Импортируем встроенный модуль http .
2. [Link]((req, res) => { ... }); : Создаем HTTP-сервер. Коллбэк-функция будет
вызываться каждый раз, когда на сервер приходит новый запрос. req содержит
информацию о входящем запросе, а res используется для отправки ответа
клиенту.
3. [Link] = 200; : Устанавливаем HTTP-статус ответа (200 OK).
4. [Link]('Content-Type', 'application/json'); : Устанавливаем заголовок Content-Type ,
указывая, что мы отправляем JSON.
5. if ([Link] === '/hello' && [Link] === 'GET') { ... } : Проверяем URL и метод запроса,
чтобы реализовать простую маршрутизацию.
6. [Link]([Link](responseData)); : Отправляем ответ клиенту. [Link]()
преобразует JavaScript-объект в JSON-строку.
7. [Link](port, hostname, () => { ... }); : Запускаем сервер на указанном порту и хосте.
Коллбэк-функция выполняется, когда сервер начинает слушать входящие
соединения.
Этот простой пример демонстрирует, как [Link] обрабатывает запросы асинхронно.
Каждый входящий запрос вызывает коллбэк, который выполняется Event Loop.
[Link] не блокируется, ожидая завершения одного запроса, а продолжает слушать
новые.
1.4 Промежуточный проект: Мини-API на [Link]/Express
Теперь, когда мы освоили основы [Link] и понимаем, как работает HTTP-сервер,
давайте создадим наш первый мини-API с использованием [Link]. Express — это
минималистичный и гибкий фреймворк для веб-приложений [Link], который
предоставляет надежный набор функций для разработки веб- и мобильных
приложений. Он значительно упрощает маршрутизацию, обработку запросов и
ответов.
Цель: Создать простой API с маршрутом /hello , который будет возвращать JSON-
ответ.
Шаги:
1. Инициализация проекта:
Создайте новую папку для проекта и перейдите в нее:
2. Установка Express:
Установите [Link] как зависимость проекта:
3. Создание файла [Link] :
Создайте файл [Link] в корневой директории проекта и добавьте следующий
код:
4. Запуск сервера:
Добавьте скрипт start в ваш [Link] :
JSON
"scripts": {
"start": "node [Link]",
"test": "echo \"Error: no test specified\" && exit 1"
},
Plain Text
Теперь вы можете запустить сервер, выполнив в терминале:
```bash
npm start
```
Вы должны увидеть сообщение в консоли: `Express API запущен по адресу
[Link]
5. Тестирование API:
Откройте ваш браузер и перейдите по адресу [Link] . Вы должны
увидеть JSON-ответ:
json
{
"message": "Привет от Express API!",
"timestamp": "2025-07-05T[Link].000Z" // Время будет отличаться
}
Также попробуйте перейти по [Link] .
Plain Text
Этот мини-проект демонстрирует базовую настройку [Link], создание
маршрута и отправку JSON-ответа. Это будет нашей заготовкой для дальнейшей
работы и расширения в последующих главах.
1.5 Практические задания по Главе 1
Для закрепления материала и развития практических навыков, выполните
следующие задания. Не спешите, старайтесь понять каждую деталь. Решения и
ответы будут предоставлены в отдельных файлах.
1. Настроить проект:
• Создайте новую папку для проекта (например, chapter1_exercises ).
• Инициализируйте [Link] с помощью npm init -y (флаг -y отвечает на все
вопросы по умолчанию).
• Установите любую простую библиотеку (например, lodash ) как зависимость,
используя npm install lodash .
• Создайте файл [Link] и импортируйте lodash . Используйте одну из его
функций (например, _.chunk ) и выведите результат в консоль.
2. Работа с современными фичами JS (ES6+):
• Напишите функцию createMultiplier(factor) , которая возвращает другую
функцию. Возвращаемая функция должна принимать число и умножать его на
factor . Используйте замыкания.
• Создайте асинхронную функцию fetchRandomUser() , которая использует
async/await для получения данных о случайном пользователе с публичного API
(например, [Link] ). Функция должна возвращать имя и
фамилию пользователя. Обработайте возможные ошибки с помощью try/catch .
• Определите класс Book с конструктором, принимающим title и author .
Добавьте метод getSummary() , который возвращает строку вида "Название:
[title], Автор: [author]". Создайте несколько экземпляров класса и вызовите
метод.
3. Создать простой HTTP-сервер на [Link]:
• Используя только встроенный модуль http (без Express), создайте сервер,
который слушает порт 4000.
• Сервер должен отвечать на GET-запрос по пути /info JSON-объектом,
содержащим ваше имя и текущую дату/время.
• Для всех остальных запросов сервер должен возвращать статус 404 и JSON-
объект { "error": "Not Found" } .
4. Попрактиковаться с Git:
• В папке chapter1_exercises инициализируйте Git-репозиторий ( git init ).
• Создайте файл [Link] с кратким описанием проекта и сделайте первый
коммит ( git add . , git commit -m "Initial commit" ).
• Создайте новую ветку feature/add-contact-info .
• В этой ветке добавьте в [Link] секцию "Contact Info" с вашим email.
Сделайте коммит.
• Переключитесь обратно на основную ветку ( main или master ).
• Объедините изменения из feature/add-contact-info в основную ветку ( git merge
feature/add-contact-info ).
• Проверьте, что изменения появились в [Link] в основной ветке.
Глава 2: Разработка фронтенда на React
В этой главе мы погрузимся в мир React — одной из самых популярных JavaScript-
библиотек для создания пользовательских интерфейсов. React позволяет
разрабатывать сложные и интерактивные UI, разбивая их на небольшие,
изолированные и переиспользуемые компоненты. Мы начнем с основ, таких как JSX и
функциональные компоненты, и постепенно перейдем к более продвинутым
концепциям, включая хуки, управление состоянием и маршрутизацию.
React фокусируется на декларативном подходе к построению UI. Это означает, что вы
описываете, как должен выглядеть ваш UI в зависимости от состояния, а React берет
на себя заботу об эффективном обновлении DOM (Document Object Model) при
изменении данных. Такой подход значительно упрощает разработку и отладку
сложных интерфейсов.
Мы изучим, как организовывать проект на React, разделяя его на логические
компоненты, и как управлять данными внутри этих компонентов. Особое внимание
уделим управлению состоянием, поскольку это ключевой аспект любого
интерактивного приложения. Мы рассмотрим встроенные механизмы React, такие как
Context API, а также популярные сторонние библиотеки, такие как Redux, которые
помогают управлять глобальным состоянием в больших приложениях. Также мы
научимся подгружать данные с API, которые мы будем создавать на бэкенде, и
динамически обновлять интерфейс в ответ на эти данные. Важно будет
спроектировать компоненты с учётом масштабируемости и переиспользуемости,
чтобы ваш код был чистым и легко поддерживаемым.
2.1 JSX и компоненты React
2.1.1 Что такое JSX?
JSX (JavaScript XML) — это синтаксическое расширение JavaScript, которое позволяет
писать HTML-подобный код прямо внутри JavaScript-файлов. React использует JSX для
описания структуры пользовательского интерфейса. Хотя JSX не является
обязательным для использования React (можно писать UI, используя только
JavaScript), он значительно упрощает и ускоряет разработку, делая код более
читаемым и интуитивно понятным.
Пример JSX:
JSX
const element = <h1>Привет, мир!</h1>;
Этот код выглядит как HTML, но на самом деле это JavaScript. JSX-выражения
компилируются (транспилируются) в обычные JavaScript-вызовы функций (например,
[Link]() ) с помощью таких инструментов, как Babel. Браузеры не
понимают JSX напрямую, поэтому этот шаг компиляции необходим.
Основные правила JSX:
• Возвращать один корневой элемент: JSX-выражение должно возвращать только
один корневой элемент. Если вам нужно вернуть несколько элементов, их
необходимо обернуть в общий родительский элемент, например, <div> или
[Link] (сокращенно <>...</> ).
• Использование JavaScript-выражений: Внутри JSX можно вставлять JavaScript-
выражения, заключая их в фигурные скобки {} . Это позволяет динамически
отображать данные, вызывать функции и использовать условную логику.
const name = 'Алиса';
const element = <h1>Привет, {name}!</h1>;
• Атрибуты: Атрибуты в JSX пишутся в camelCase (например, className вместо
class , htmlFor вместо for ). Значения строковых атрибутов заключаются в
кавычки, а JavaScript-вывыражения — в фигурные скобки.
• Самозакрывающиеся теги: Элементы без дочерних элементов должны быть
самозакрывающимися (например, <img /> , <input /> ).
2.1.2 Функциональные компоненты
В React компоненты — это строительные блоки UI. Они позволяют разбивать
интерфейс на независимые, переиспользуемые части. В современном React
предпочтительнее использовать функциональные компоненты в сочетании с
хуками (Hooks).
Функциональный компонент — это обычная JavaScript-функция, которая принимает
объект props (свойства) в качестве аргумента и возвращает JSX-элемент.
Пример функционального компонента:
JSX
import React from 'react';
function WelcomeMessage(props) {
return <h1>Привет, {[Link]}!</h1>;
}
// Использование компонента
// <WelcomeMessage name="Мир" />
Props (свойства) — это способ передачи данных от родительского компонента к
дочернему. Props доступны только для чтения внутри компонента, что делает
компоненты предсказуемыми и легкими для тестирования. Компонент никогда не
должен изменять свои собственные props .
Пример передачи и использования props:
JSX
import React from 'react';
function UserCard(props) {
return (
<div>
<h2>{[Link]}</h2>
<p>Возраст: {[Link]}</p>
<p>Email: {[Link]}</p>
</div>
);
}
function App() {
const userData = {
name: 'Иван Петров',
age: 28,
email: '[Link]@[Link]'
};
return (
<div>
<h1>Информация о пользователе</h1>
<UserCard user={userData} />
</div>
);
}
// export default App; // Обычно экспортируется корневой компонент
В этом примере компонент App передает объект userData в компонент UserCard
через пропс user . Компонент UserCard затем использует данные из этого пропса для
отображения информации о пользователе.
2.2 Хуки (Hooks)
Хуки — это функции, которые позволяют использовать состояние и другие
возможности React без написания классов. Они были введены в React 16.8 и стали
стандартным способом написания функциональных компонентов. Хуки решают
проблему сложности классов, особенно при работе с логикой жизненного цикла и
переиспользуемой логикой.
2.2.1 useState (Состояние компонента)
Хук useState позволяет добавить состояние в функциональные компоненты. Он
возвращает массив из двух элементов: текущее значение состояния и функцию для
его обновления.
Синтаксис:
JavaScript
const [state, setState] = useState(initialState);
• state : Текущее значение состояния.
• setState : Функция для обновления состояния. При вызове этой функции React
перерендерит компонент с новым значением состояния.
• initialState : Начальное значение состояния. Может быть любым типом данных
(число, строка, объект, массив и т.д.).
Пример использования useState для счетчика:
JSX
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0); // Инициализируем состояние count
значением 0
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<p>Счетчик: {count}</p>
<button onClick={increment}>Увеличить</button>
<button onClick={decrement}>Уменьшить</button>
</div>
);
}
// export default Counter;
Важные моменты при работе с useState :
• Не изменяйте состояние напрямую: Всегда используйте функцию setCount (или
аналогичную) для обновления состояния. Прямое изменение count = 5 не
вызовет перерендеринга компонента.
• Асинхронное обновление: Обновления состояния могут быть асинхронными.
Если новое состояние зависит от предыдущего, используйте функциональную
форму setCount(prevCount => prevCount + 1) .
• Множественные состояния: Вы можете использовать useState несколько раз в
одном компоненте для управления разными частями состояния.
2.2.2 useEffect (Эффекты жизненного цикла)
Хук useEffect позволяет выполнять «побочные эффекты» в функциональных
компонентах. Побочные эффекты — это операции, которые взаимодействуют с
внешним миром или влияют на него, например, запросы к API, подписки на события,
изменение DOM вручную, таймеры и т.д. useEffect заменяет методы жизненного
цикла, такие как componentDidMount , componentDidUpdate и componentWillUnmount .
Синтаксис:
JavaScript
useEffect(() => {
// Код эффекта
return () => {
// Функция очистки (выполняется при размонтировании или перед следующим
эффектом)
};
}, [dependencies]); // Массив зависимостей
• Функция эффекта: Первый аргумент useEffect — это функция, которая содержит
код побочного эффекта. Она выполняется после каждого рендера компонента (по
умолчанию).
• Функция очистки: Необязательная функция, которая возвращается из функции
эффекта. Она выполняется перед тем, как компонент будет размонтирован, или
перед каждым повторным выполнением эффекта (если зависимости изменились).
Используется для отмены подписок, очистки таймеров и т.д.
• Массив зависимостей: Второй аргумент useEffect — это необязательный массив
зависимостей. Он контролирует, когда эффект должен быть повторно выполнен.
• Если массив пуст ( [] ), эффект выполняется только один раз после первого
рендера (аналог componentDidMount ).
• Если массив опущен, эффект выполняется после каждого рендера компонента.
• Если массив содержит переменные, эффект выполняется при первом рендере и
каждый раз, когда значение любой из этих переменных изменяется (аналог
componentDidMount + componentDidUpdate ).
Пример использования useEffect для загрузки данных:
JSX
import React, { useState, useEffect } from 'react';
function DataFetcher() {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState(null);
useEffect(() => {
const fetchData = async () => {
try {
const response = await
fetch('[Link]
if (![Link]) {
throw new Error(`HTTP error! status: ${[Link]}`);
}
const result = await [Link]();
setData(result);
} catch (err) {
setError(err);
} finally {
setLoading(false);
}
};
fetchData();
// Функция очистки (не нужна в этом примере, но важна для подписок)
return () => {
// Например, отмена запроса или очистка таймера
};
}, []); // Пустой массив зависимостей: эффект выполнится только один раз
при монтировании
if (loading) return <p>Загрузка данных...</p>;
if (error) return <p>Ошибка: {[Link]}</p>;
return (
<div>
<h2>Полученные данные:</h2>
<pre>{[Link](data, null, 2)}</pre>
</div>
);
}
// export default DataFetcher;
В этом примере useEffect используется для выполнения HTTP-запроса к API после
первого рендера компонента. Пустой массив зависимостей [] гарантирует, что
эффект будет выполнен только один раз, имитируя поведение componentDidMount .
2.2.3 Другие полезные хуки ( useContext , useRef , useCallback , useMemo )
React предоставляет и другие встроенные хуки, которые помогают решать различные
задачи:
• useContext : Позволяет компоненту подписаться на изменения контекста React.
Контекст предоставляет способ передавать данные через дерево компонентов
без необходимости передавать пропсы вручную на каждом уровне. Это полезно
для глобальных данных, таких как тема, язык или информация об
аутентификации.
• useRef : Возвращает изменяемый объект ref , свойство .current которого
инициализируется переданным аргументом ( initialValue ). Возвращенный объект
будет сохраняться в течение всего времени жизни компонента. useRef часто
используется для прямого доступа к DOM-элементам или для хранения
изменяемых значений, которые не должны вызывать перерендер компонента при
изменении.
• useCallback : Возвращает мемоизированную версию коллбэка. Это полезно для
оптимизации производительности, когда вы передаете коллбэки дочерним
компонентам, которые оптимизированы с помощью [Link] . useCallback
предотвращает ненужные перерендеры дочерних компонентов, если коллбэк не
изменился между рендерами.
• useMemo : Возвращает мемоизированное значение. Это полезно для
оптимизации производительности, когда вы выполняете дорогостоящие
вычисления, и вам нужно, чтобы они пересчитывались только тогда, когда
изменяются их зависимости. useMemo кэширует результат функции и
пересчитывает его только при изменении зависимостей.
2.3 React Router (SPA)
React Router — это стандартная библиотека для маршрутизации в React-
приложениях. Она позволяет создавать одностраничные приложения (SPA - Single
Page Applications), где навигация между различными
страницами происходит без перезагрузки всей страницы. Это улучшает
пользовательский опыт, делая переходы мгновенными и плавными.
2.3.1 Что такое SPA и зачем нужна маршрутизация?
Одностраничное приложение (SPA) — это веб-приложение, которое загружает одну
HTML-страницу и динамически обновляет ее содержимое по мере взаимодействия
пользователя с приложением. В отличие от традиционных многостраничных
приложений (MPA), где каждый переход по ссылке приводит к полной перезагрузке
страницы с сервера, SPA перехватывает навигацию и изменяет только необходимые
части DOM, используя JavaScript.
Преимущества SPA:
• Быстрые переходы: После первоначальной загрузки страницы, переходы между
разделами происходят мгновенно, так как не требуется полная перезагрузка.
• Лучший пользовательский опыт: Ощущение более плавного и отзывчивого
интерфейса, похожего на десктопные приложения.
• Разделение фронтенда и бэкенда: Фронтенд (React) и бэкенд ([Link]/Express)
могут разрабатываться и развертываться независимо, взаимодействуя через API.
Маршрутизация в контексте SPA — это механизм, который связывает URL-адреса с
определенными компонентами или представлениями в приложении. Когда
пользователь переходит по URL, маршрутизатор определяет, какой компонент
должен быть отображен, и обновляет UI без перезагрузки страницы.
2.3.2 Установка и базовое использование React Router
Для начала работы с React Router необходимо установить его пакет:
Bash
npm install react-router-dom
Основные компоненты React Router:
• <BrowserRouter> : Использует History API HTML5 для синхронизации UI с URL. Это
рекомендуемый роутер для веб-приложений.
• <Routes> : Контейнер для всех <Route> компонентов. Он выбирает первый
<Route> , который соответствует текущему URL.
• <Route> : Определяет соответствие между URL-путем и компонентом, который
должен быть отображен.
• <Link> : Компонент для навигации. Он рендерится как HTML-тег <a> , но
предотвращает полную перезагрузку страницы.
• <NavLink> : Специальная версия <Link> , которая добавляет стили активной
ссылке.
Пример базовой маршрутизации:
JSX
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-
dom';
// Компоненты страниц
function HomePage() {
return <h2>Главная страница</h2>;
}
function AboutPage() {
return <h2>О нас</h2>;
}
function ContactPage() {
return <h2>Контакты</h2>;
}
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/about">О нас</Link>
</li>
<li>
<Link to="/contact">Контакты</Link>
</li>
</ul>
</nav>
{/* Определяем маршруты */}
<Routes>
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
<Route path="/" element={<HomePage />} />
</Routes>
</div>
</Router>
);
}
// export default App;
В этом примере:
1. Мы оборачиваем все приложение в <Router> .
2. Создаем навигационное меню с помощью <Link> компонентов. to пропс
указывает путь, на который нужно перейти.
3. Используем <Routes> для группировки всех маршрутов.
4. Каждый <Route> определяет path (URL-путь) и element (компонент, который
будет отображен при совпадении пути).
2.3.3 Параметры маршрутов и вложенные маршруты
Часто требуется передавать данные через URL, например, ID статьи или
пользователя. React Router позволяет использовать параметры маршрутов.
Пример с параметром маршрута:
JSX
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link, useParams } from
'react-router-dom';
function UserProfile() {
const { userId } = useParams(); // Хук useParams позволяет получить
параметры из URL
return <h2>Профиль пользователя: {userId}</h2>;
}
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/users/1">Пользователь 1</Link>
</li>
<li>
<Link to="/users/2">Пользователь 2</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/users/:userId" element={<UserProfile />} /> {/*
:userId - это параметр */}
<Route path="/" element={<HomePage />} />
</Routes>
</div>
</Router>
);
}
// export default App;
Вложенные маршруты позволяют создавать иерархическую структуру URL и
отображать компоненты внутри других компонентов. Это полезно для таких вещей,
как вкладки или разделы внутри страницы.
JSX
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link, Outlet } from 'react-
router-dom';
function Dashboard() {
return (
<div>
<h2>Панель управления</h2>
<nav>
<ul>
<li>
<Link to="/dashboard/profile">Мой профиль</Link>
</li>
<li>
<Link to="/dashboard/settings">Настройки</Link>
</li>
</ul>
</nav>
<hr />
<Outlet /> {/* Здесь будут рендериться вложенные маршруты */}
</div>
);
}
function DashboardProfile() {
return <h3>Мой профиль</h3>;
}
function DashboardSettings() {
return <h3>Настройки</h3>;
}
function App() {
return (
<Router>
<div>
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/dashboard">Панель управления</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/dashboard" element={<Dashboard />}>
{/* Вложенные маршруты */}
<Route path="profile" element={<DashboardProfile />} />
<Route path="settings" element={<DashboardSettings />} />
</Route>
<Route path="/" element={<HomePage />} />
</Routes>
</div>
</Router>
);
}
// export default App;
В этом примере компонент Dashboard содержит <Outlet /> , который является местом,
где будут рендериться его дочерние маршруты ( profile и settings ).
2.3.4 Программная навигация ( useNavigate )
Иногда требуется программно перенаправить пользователя на другую страницу,
например, после успешной отправки формы или аутентификации. Для этого
используется хук useNavigate .
JSX
import React from 'react';
import { useNavigate } from 'react-router-dom';
function LoginForm() {
const navigate = useNavigate();
const handleSubmit = (event) => {
[Link]();
// Логика аутентификации...
const isAuthenticated = true; // Предположим, аутентификация успешна
if (isAuthenticated) {
navigate('/dashboard'); // Перенаправляем на панель управления
}
};
return (
<form onSubmit={handleSubmit}>
{/* Поля формы */}
<button type="submit">Войти</button>
</form>
);
}
// export default LoginForm;
2.4 Управление состоянием (Context API, Redux/другие)
Управление состоянием — одна из самых важных и сложных задач в больших React-
приложениях. По мере роста приложения, передача пропсов через множество
уровней компонентов ( prop drilling ) становится громоздкой и неудобной. Для
решения этой проблемы существуют различные подходы и библиотеки.
2.4.1 Context API
Context API — это встроенный в React механизм, который позволяет передавать
данные через дерево компонентов без необходимости передавать пропсы вручную
на каждом уровне. Он идеально подходит для "глобальных" данных, таких как
текущая тема, язык, информация об аутентификации пользователя или другие
данные, которые должны быть доступны многим компонентам на разных уровнях
вложенности.
Основные шаги использования Context API:
1. Создание контекста: Используйте [Link]() для создания объекта
контекста. Он возвращает объект с двумя компонентами: Provider и Consumer
(или вы можете использовать хук useContext ).
2. Предоставление значения: Компонент Provider используется для
"предоставления" значения контекста всем дочерним компонентам, которые
находятся внутри него. Он принимает пропс value .
3. Использование значения: Дочерние компоненты могут "потреблять" значение
контекста с помощью хука useContext .
Пример использования Context API для управления темой:
[Link] :
JSX
import React, { createContext, useState, useContext } from 'react';
const ThemeContext = createContext(null);
export const ThemeProvider = ({ children }) => {
const [theme, setTheme] = useState('light');
const toggleTheme = () => {
setTheme(prevTheme => (prevTheme === 'light' ? 'dark' : 'light'));
};
return (
<[Link] value={{ theme, toggleTheme }}>
{children}
</[Link]>
);
};
export const useTheme = () => {
const context = useContext(ThemeContext);
if (context === undefined) {
throw new Error('useTheme must be used within a ThemeProvider');
}
return context;
};
[Link] :
JSX
import React from 'react';
import { ThemeProvider, useTheme } from './ThemeContext';
function ThemeToggle() {
const { theme, toggleTheme } = useTheme();
return (
<button onClick={toggleTheme}>
Переключить тему: {theme === 'light' ? 'Светлая' : 'Темная'}
</button>
);
}
function ThemedComponent() {
const { theme } = useTheme();
return (
<div style={{
background: theme === 'dark' ? '#333' : '#FFF',
color: theme === 'dark' ? '#FFF' : '#333',
padding: '20px',
margin: '10px 0',
borderRadius: '5px'
}}>
<p>Это компонент с темой: {theme}</p>
</div>
);
}
function App() {
return (
<ThemeProvider>
<h1>Пример Context API</h1>
<ThemeToggle />
<ThemedComponent />
<ThemedComponent />
</ThemeProvider>
);
}
// export default App;
Context API очень полезен для нечасто обновляемых данных, которые нужны многим
компонентам. Однако для сложного глобального состояния с частыми обновлениями
и сложной логикой, Context API может стать менее эффективным, так как каждое
обновление контекста приводит к перерендеру всех компонентов, которые его
потребляют.
2.4.2 Redux и другие библиотеки управления состоянием
Для более сложных сценариев управления состоянием, особенно в больших
приложениях с большим количеством взаимодействий и асинхронных операций,
часто используются специализированные библиотеки.
Redux — это предсказуемый контейнер состояния для JavaScript-приложений. Он
основан на трех основных принципах:
1. Единый источник истины: Все состояние приложения хранится в одном объекте,
называемом "стором" (store).
2. Состояние только для чтения: Единственный способ изменить состояние — это
отправить "действие" (action) — простой JavaScript-объект, описывающий, что
произошло.
3. Изменения выполняются чистыми функциями: Для описания того, как
состояние изменяется в ответ на действия, используются "редюсеры" (reducers) —
чистые функции, которые принимают текущее состояние и действие, и
возвращают новое состояние.
Основные концепции Redux:
• Store: Объект, который хранит состояние всего приложения. У него есть методы
getState() , dispatch(action) и subscribe(listener) .
• Actions: Простые JavaScript-объекты, которые описывают, что произошло. Они
должны иметь свойство type .
• Reducers: Чистые функции, которые принимают текущее состояние и действие, и
возвращают новое состояние. Они не должны изменять исходное состояние, а
должны возвращать новый объект состояния.
• Dispatch: Метод стора, который отправляет действие в редюсеры.
• Selectors: Функции, которые извлекают определенные части состояния из стора.
Redux Toolkit — это официальный, рекомендуемый набор инструментов для
эффективной разработки с Redux. Он упрощает многие рутинные задачи и включает
в себя такие утилиты, как configureStore , createSlice и createAsyncThunk , которые
значительно сокращают количество "шаблонного" кода.
Пример Redux с Redux Toolkit (упрощенно):
1. Установка:
2. Создание "среза" (slice) состояния:
features/counter/[Link] :
3. Настройка стора:
app/[Link] :
4. Предоставление стора приложению:
[Link] (или [Link] ):
5. Использование состояния в компонентах:
[Link] (или любой другой компонент):
Когда использовать Redux (или аналоги):
• Большое количество глобального состояния.
• Состояние часто обновляется.
• Логика обновления состояния сложна и включает асинхронные операции.
• Несколько компонентов нуждаются в доступе к одному и тому же состоянию.
• Требуется предсказуемость и легкая отладка состояния (Redux DevTools).
Другие популярные библиотеки управления состоянием:
• Zustand: Легковесная, быстрая и простая в использовании библиотека для
управления состоянием. Использует хуки и минималистичный API.
• Recoil: Разработан Facebook, ориентирован на атомарное управление
состоянием, что позволяет более гранулярно отслеживать изменения и
оптимизировать рендеры.
• Jotai: Еще одна минималистичная библиотека, основанная на атомах, похожая на
Recoil, но с еще более простым API.
• SWR / React Query: Эти библиотеки в первую очередь предназначены для
управления состоянием кэширования данных, полученных с сервера. Они
значительно упрощают работу с асинхронными данными, повторными
запросами, кэшированием и синхронизацией.
Выбор библиотеки зависит от размера и сложности вашего проекта. Для небольших и
средних приложений Context API часто бывает достаточно. Для больших и сложных
приложений Redux (с Redux Toolkit) или более современные альтернативы, такие как
Zustand или Recoil, могут быть более подходящими.
2.5 Стилизация компонентов
В React-приложениях существует множество способов стилизации компонентов, от
традиционного CSS до современных решений, таких как CSS-in-JS. Выбор метода
зависит от предпочтений команды, размера проекта и требований к
производительности.
2.5.1 Обычный CSS и CSS-модули
Обычный CSS: Вы можете использовать стандартные CSS-файлы, как и в любом
другом веб-проекте. Просто импортируйте их в ваш JavaScript-файл:
JSX
// [Link]
.header {
color: blue;
font-size: 24px;
}
// [Link]
import React from 'react';
import './[Link]';
function App() {
return <h1 className="header">Привет, React!</h1>;
}
// export default App;
Проблема: В больших проектах с обычным CSS легко возникают конфликты имен
классов, так как все стили находятся в глобальной области видимости.
CSS-модули: Решают проблему глобальной области видимости CSS, автоматически
генерируя уникальные имена классов. Это позволяет инкапсулировать стили в
пределах компонента, предотвращая конфликты. CSS-модули обычно
поддерживаются сборщиками, такими как Webpack или Parcel.
[Link] :
CSS
.container {
background-color: #f0f0f0;
padding: 20px;
border-radius: 8px;
}
.title {
color: #333;
font-size: 20px;
}
[Link] :
JSX
import React from 'react';
import styles from './[Link]'; // Импортируем стили как
объект
function MyComponent() {
return (
<div className={[Link]}>
<h2 className={[Link]}>Заголовок компонента</h2>
<p>Это текст внутри компонента.</p>
</div>
);
}
// export default MyComponent;
При использовании CSS-модулей, [Link] будет скомпилирован в уникальное
имя класса, например, MyComponent_container__abc12 . Это гарантирует, что стили этого
компонента не повлияют на другие компоненты.
2.5.2 Styled Components (CSS-in-JS)
Styled Components — это популярная библиотека, которая позволяет писать CSS-код
прямо внутри JavaScript-файлов, используя тегированные шаблонные литералы. Это
подход "CSS-in-JS", который обеспечивает полную инкапсуляцию стилей и
динамическую стилизацию на основе пропсов.
Преимущества Styled Components:
• Инкапсуляция: Стили привязаны к компонентам и не конфликтуют с другими
стилями.
• Динамические стили: Легко изменять стили на основе пропсов или состояния
компонента.
• Удаление неиспользуемого CSS: Инструменты сборки могут легко определить и
удалить неиспользуемые стили.
• Улучшенная читаемость: Стили находятся рядом с логикой компонента.
Установка:
Bash
npm install styled-components
Пример использования Styled Components:
JSX
import React from 'react';
import styled from 'styled-components';
// Создаем стилизованный компонент Button
const Button = [Link]`
background: ${props => [Link] ? '#007bff' : 'white'};
color: ${props => [Link] ? 'white' : '#007bff'};
font-size: 1em;
margin: 1em;
padding: 0.25em 1em;
border: 2px solid #007bff;
border-radius: 3px;
cursor: pointer;
&:hover {
opacity: 0.8;
}
`;
// Создаем стилизованный компонент Input
const Input = [Link]`
padding: 0.5em;
margin: 0.5em;
color: palevioletred;
background: papayawhip;
border: none;
border-radius: 3px;
`;
function StyledComponentsExample() {
return (
<div>
<Button>Обычная кнопка</Button>
<Button primary>Основная кнопка</Button>
<Input placeholder="Введите текст" type="text" />
</div>
);
}
// export default StyledComponentsExample;
В этом примере мы создаем стилизованные компоненты Button и Input с помощью
[Link] и [Link] . Мы можем передавать пропсы (например, primary ) в
стилизованные компоненты и использовать их для динамического изменения стилей.
Другие подходы к стилизации:
• Tailwind CSS: Утилитарный CSS-фреймворк, который предоставляет
низкоуровневые классы для быстрого построения пользовательских
интерфейсов. Вместо написания пользовательского CSS, вы комбинируете классы
Tailwind прямо в JSX.
• Material-UI / Ant Design: Библиотеки UI-компонентов, которые предоставляют
готовые, стилизованные компоненты, соответствующие определенным дизайн-
системам (Material Design, Ant Design). Они значительно ускоряют разработку, но
могут быть менее гибкими в кастомизации.
• Sass/Less: Препроцессоры CSS, которые добавляют такие функции, как
переменные, вложенность, миксины и функции, что делает CSS более мощным и
поддерживаемым.
Выбор метода стилизации зависит от ваших потребностей. Для небольших проектов
CSS-модули или обычный CSS могут быть достаточными. Для средних и больших
проектов Styled Components или Tailwind CSS предлагают более мощные и гибкие
решения.
2.6 Практические задания по Главе 2
Для закрепления материала и развития практических навыков, выполните
следующие задания. Старайтесь применять все изученные концепции.
1. Создать несколько React-компонентов:
• Создайте новый React-проект с помощью npx create-react-app my-react-app (или
vite для более быстрого старта).
• Создайте функциональный компонент Header , который принимает пропс
title и отображает его в теге <h1> .
• Создайте функциональный компонент Footer , который отображает текущий
год и имя автора (ваше имя).
• Создайте компонент ProductCard , который принимает пропсы name , price и
imageUrl и отображает информацию о продукте.
• В главном компоненте App используйте эти компоненты, передавая им
соответствующие пропсы. Отобразите несколько ProductCard .
2. Реализовать «ToDo List» с добавлением/удалением задач:
• Создайте компонент TodoList .
• Используйте хук useState для управления списком задач (массив объектов,
каждый объект имеет id , text , completed ).
• Реализуйте поле ввода и кнопку для добавления новых задач. При добавлении
задачи ей должен присваиваться уникальный id .
• Отобразите список задач. Для каждой задачи должна быть кнопка "Удалить" и
чекбокс для отметки о выполнении.
• Используйте useEffect для загрузки начального списка задач из localStorage
при монтировании компонента и сохранения списка в localStorage при каждом
изменении.
3. Настроить React Router:
• В вашем React-проекте установите react-router-dom .
• Создайте три страницы-компонента: HomePage , AboutPage , DashboardPage .
• Настройте маршрутизацию так, чтобы:
• / отображал HomePage .
• /about отображал AboutPage .
• /dashboard отображал DashboardPage .
• Добавьте навигационное меню с помощью <Link> или <NavLink> для перехода
между страницами.
• На DashboardPage реализуйте вложенные маршруты: /dashboard/profile и
/dashboard/settings , отображающие соответствующие компоненты ProfileSettings
и AccountSettings . Используйте <Outlet /> .
4. Попробовать использовать Context API или Redux для глобального состояния:
• Вариант 1 (Context API): Реализуйте контекст для управления состоянием
пользователя (например, isLoggedIn: boolean , username: string ).
• Создайте [Link] с [Link] и хуком useAuth .
• Оберните ваше приложение в [Link] .
• Создайте компонент LoginButton , который будет переключать isLoggedIn и
отображать имя пользователя, если он авторизован. Используйте useAuth .
• Создайте компонент ProtectedContent , который будет отображаться только
если isLoggedIn равно true .
• Вариант 2 (Redux Toolkit): Реализуйте стор Redux для управления состоянием
счетчика.
• Настройте Redux Toolkit, создав "срез" для счетчика с действиями increment ,
decrement и reset .
• Создайте компонент ReduxCounter , который будет отображать значение
счетчика и кнопки для его изменения, используя useSelector и useDispatch .
• Интегрируйте стор Redux в ваше приложение.
5. Стилизация компонентов:
• Выберите один из компонентов, созданных в задании 1 (например,
ProductCard ).
• Стилизуйте его, используя CSS-модули. Создайте [Link] и
примените стили к элементам компонента.
• Создайте еще один компонент (например, StyledButton ) и стилизуйте его,
используя Styled Components. Добавьте пропс, который будет менять цвет
кнопки (например, primary ).
Глава 3: Бэкенд на [Link] и Express
После того как мы освоили основы фронтенд-разработки с React, пришло время
перейти к серверной части нашего Full-Stack приложения. В этой главе мы углубимся
в разработку бэкенда с использованием [Link] и [Link]. [Link], как мы уже знаем,
позволяет выполнять JavaScript на стороне сервера, а [Link] — это
минималистичный и гибкий фреймворк, который предоставляет мощный набор
функций для создания веб-приложений и API.
Мы разберем архитектуру приложений на [Link], повторим концепцию
неблокирующего I/O и Event Loop, а также научимся работать с файловой системой и
процессами. Основное внимание будет уделено созданию RESTful API — стандартного
подхода к взаимодействию между клиентом (нашим React-приложением) и сервером.
Мы научимся обрабатывать различные HTTP-методы (GET, POST, PUT, DELETE),
работать с параметрами маршрутов и телами запросов.
Особое внимание будет уделено архитектуре серверного кода: как правильно
организовывать маршруты, контроллеры и middleware. Middleware в Express — это
мощный механизм для выполнения различных задач, таких как логирование,
аутентификация, валидация данных и обработка ошибок, до или после обработки
основного запроса. Также мы рассмотрим основы безопасности на бэкенде, включая
валидацию входных данных, обработку ошибок и использование заголовков
безопасности. Начнем знакомство с аутентификацией, реализовав простую схему с
использованием JWT (JSON Web Token), которая является одним из самых
распространенных способов аутентификации в современных веб-приложениях.
3.1 Основы [Link] (повторение и углубление)
Мы уже кратко касались основ [Link] в Главе 1, но теперь, когда мы переходим к
полноценной бэкенд-разработке, важно углубиться в некоторые ключевые
концепции.
3.1.1 Event Loop и неблокирующий I/O (повторение)
Как мы помним, [Link] использует однопоточную, неблокирующую модель I/O,
основанную на Event Loop. Это означает, что [Link] не создает новый поток для
каждого входящего запроса, а обрабатывает все операции в одном потоке, используя
асинхронные коллбэки. Когда происходит операция ввода/вывода (например, чтение
из базы данных, запрос к внешнему API, чтение файла), [Link] передает эту
операцию в системное ядро и продолжает выполнять следующий код. Как только
операция завершается, ее коллбэк помещается в очередь событий, и Event Loop
забирает его, когда основной поток свободен.
Почему это важно для бэкенда?
Эта модель делает [Link] чрезвычайно эффективным для приложений, которые
обрабатывают большое количество одновременных соединений и выполняют много
операций ввода/вывода (например, API-серверы, чаты в реальном времени). Однако,
если в вашем коде есть "тяжелые" синхронные операции (например, сложные
вычисления, которые занимают много времени), они будут блокировать Event Loop и
замедлять обработку всех остальных запросов. Поэтому важно писать асинхронный
код и использовать неблокирующие операции везде, где это возможно.
3.1.2 Модули fs (файловая система) и http (повторение)
[Link] предоставляет множество встроенных модулей для работы с различными
аспектами операционной системы и сетевых протоколов. Мы уже использовали
модуль http для создания простого HTTP-сервера. Теперь давайте кратко
рассмотрим модуль fs для работы с файловой системой.
Модуль fs (File System):
Модуль fs предоставляет API для взаимодействия с файловой системой. Он
поддерживает как синхронные, так и асинхронные методы. В [Link] всегда
предпочтительнее использовать асинхронные методы, чтобы не блокировать Event
Loop.
Пример асинхронного чтения файла:
JavaScript
const fs = require("fs");
[Link]("[Link]", "utf8", (err, data) => {
if (err) {
[Link]("Ошибка чтения файла:", err);
return;
}
[Link]("Содержимое файла:", data);
});
[Link]("Эта строка выполнится до чтения файла.");
Пример асинхронной записи в файл:
JavaScript
const fs = require("fs");
const content = "Привет, это новая строка!";
[Link]("[Link]", content, "utf8", (err) => {
if (err) {
[Link]("Ошибка записи в файл:", err);
return;
}
[Link]("Файл успешно записан.");
});
Модуль fs также предоставляет методы для работы с директориями, проверки
существования файлов, получения информации о файлах и многого другого. Всегда
помните о предпочтении асинхронных операций.
3.2 [Link]: Фреймворк для [Link]
[Link] — это быстрый, неблокирующий, минималистичный и гибкий фреймворк
для веб-приложений [Link], который предоставляет надежный набор функций для
разработки веб- и мобильных приложений. Он упрощает многие рутинные задачи,
связанные с обработкой HTTP-запросов, маршрутизацией, middleware и ответами.
3.2.1 Установка и базовый "Hello World"
Для начала работы с [Link] необходимо установить его в ваш проект:
Bash
npm install express
Базовый пример Express-приложения:
JavaScript
// [Link]
const express = require("express");
const app = express();
const port = 3000;
// Определение маршрута для GET-запроса на корневой URL
[Link]("/", (req, res) => {
[Link]("Привет от Express!");
});
// Запуск сервера
[Link](port, () => {
[Link](`Сервер запущен на [Link]
});
Запустите этот файл с помощью node [Link] и перейдите в браузере по адресу
[Link] . Вы увидите сообщение "Привет от Express!".
3.2.2 Маршрутизация (Routing)
Маршрутизация в Express определяет, как приложение отвечает на клиентские
запросы к определенной конечной точке (URL) и HTTP-методу (GET, POST, PUT, DELETE
и т.д.).
Определение маршрутов:
Express предоставляет методы для каждого HTTP-глагола:
• [Link](path, callback) : Обрабатывает GET-запросы.
• [Link](path, callback) : Обрабатывает POST-запросы.
• [Link](path, callback) : Обрабатывает PUT-запросы.
• [Link](path, callback) : Обрабатывает DELETE-запросы.
• [Link](path, callback): Обрабатывает запросы любого HTTP-метода.
• [Link](path, callback) : Используется для middleware (подробнее ниже).
Пример маршрутизации:
JavaScript
// [Link] (продолжение)
// GET-запрос на /users
[Link]("/users", (req, res) => {
[Link]([{ id: 1, name: "Алиса" }, { id: 2, name: "Боб" }]);
});
// POST-запрос на /users
[Link]("/users", (req, res) => {
// В реальном приложении здесь будет логика добавления пользователя в БД
[Link](201).json({ message: "Пользователь создан", user: [Link] });
});
// Маршрут с параметром: /users/:id
[Link]("/users/:id", (req, res) => {
const userId = [Link]; // Доступ к параметру через [Link]
[Link](`Получен запрос на пользователя с ID: ${userId}`);
});
// Маршрут с несколькими обработчиками (middleware)
[Link]("/example",
(req, res, next) => {
[Link]("Первый обработчик");
next(); // Передаем управление следующему обработчику
},
(req, res) => {
[Link]("Второй обработчик");
}
);
3.2.3 Middleware
Middleware — это функции, которые имеют доступ к объектам запроса ( req ), ответа
( res ) и следующей функции middleware в цикле "запрос-ответ" приложения.
Функции middleware могут:
• Выполнять любой код.
• Вносить изменения в объекты запроса и ответа.
• Завершать цикл "запрос-ответ".
• Вызывать следующую функцию middleware в стеке.
Middleware функции выполняются последовательно. Для вызова следующей функции
middleware используется функция next() .
Типы Middleware:
1. Middleware уровня приложения: Применяются ко всему приложению с помощью
[Link]() .
2. Middleware уровня маршрута: Применяются к конкретному маршруту.
3. Встроенные Middleware Express:
• [Link]() : Для отдачи статических файлов (HTML, CSS, JS, изображения).
• [Link]() : Для парсинга JSON-тела запросов.
• [Link]() : Для парсинга URL-кодированных тел запросов.
4. Сторонние Middleware: Множество пакетов npm являются middleware для Express
(например, cors , helmet , morgan ).
3.2.4 Обработка ошибок
Обработка ошибок в Express осуществляется с помощью специальных функций
middleware, которые принимают четыре аргумента: (err, req, res, next) .
JavaScript
// Middleware для обработки ошибок, должен быть последним в цепочке [Link]()
[Link]((err, req, res, next) => {
[Link]([Link]); // Логируем ошибку для отладки
[Link](500).send("Что-то пошло не так!");
});
// Пример маршрута, который может вызвать ошибку
[Link]("/error-route", (req, res, next) => {
try {
throw new Error("Принудительная ошибка!");
} catch (error) {
next(error); // Передаем ошибку в middleware обработки ошибок
}
});
3.3 Архитектура REST API (концепция CRUD)
REST (Representational State Transfer) — это архитектурный стиль для
распределенных систем, таких как веб-сервисы. Он основан на использовании
стандартных HTTP-методов для выполнения операций над ресурсами. API
(Application Programming Interface), построенный по принципам REST, называется
RESTful API.
Ключевые принципы REST:
• Ресурсы: Все в API рассматривается как ресурс (например, пользователь, пост,
комментарий). Ресурсы идентифицируются с помощью URL (Uniform Resource
Locator).
• Единообразный интерфейс: Используются стандартные HTTP-методы для
выполнения операций над ресурсами.
• Отсутствие состояния (Stateless): Каждый запрос от клиента к серверу должен
содержать всю информацию, необходимую для обработки запроса. Сервер не
должен хранить состояние клиента между запросами.
• Клиент-серверная архитектура: Четкое разделение между клиентом и сервером.
• Кэшируемость: Ответы должны быть явно или неявно помечены как кэшируемые
или некэшируемые.
CRUD-операции и HTTP-методы:
RESTful API обычно отображают основные операции с данными (CRUD: Create, Read,
Update, Delete) на соответствующие HTTP-методы:
Операция HTTP-
метод
URL-путь
(пример) Описание
Create POST /posts Создать новый пост
Read GET /posts Получить список всех постов
Read GET /posts/{id} Получить конкретный пост по
ID
Update PUT /posts/{id} Полностью обновить пост по ID
Update PATCH /posts/{id} Частично обновить пост по ID
Delete DELETE /posts/{id} Удалить пост по ID
Пример RESTful API для постов:
JavaScript
// [Link] (продолжение)
// Middleware для парсинга JSON-тела запросов
[Link]([Link]());
let posts = [
{ id: 1, title: "Мой первый пост", content: "Это содержимое первого поста."
},
{ id: 2, title: "Второй пост", content: "Содержимое второго поста." },
];
// GET /posts - Получить все посты
[Link]("/posts", (req, res) => {
[Link](posts);
});
// GET /posts/:id - Получить пост по ID
[Link]("/posts/:id", (req, res) => {
const id = parseInt([Link]);
const post = [Link]((p) => [Link] === id);
if (post) {
[Link](post);
} else {
[Link](404).json({ message: "Пост не найден" });
}
});
// POST /posts - Создать новый пост
[Link]("/posts", (req, res) => {
const newPost = {
id: [Link] > 0 ? [Link](...[Link](p => [Link])) + 1 : 1,
title: [Link],
content: [Link],
};
if (![Link] || ![Link]) {
return [Link](400).json({ message: "Заголовок и содержимое
обязательны" });
}
[Link](newPost);
[Link](201).json(newPost); // 201 Created
});
// PUT /posts/:id - Обновить пост по ID
[Link]("/posts/:id", (req, res) => {
const id = parseInt([Link]);
const postIndex = [Link]((p) => [Link] === id);
if (postIndex !== -1) {
const updatedPost = {
...posts[postIndex],
title: [Link] || posts[postIndex].title,
content: [Link] || posts[postIndex].content,
};
posts[postIndex] = updatedPost;
[Link](updatedPost);
} else {
[Link](404).json({ message: "Пост не найден" });
}
});
// DELETE /posts/:id - Удалить пост по ID
[Link]("/posts/:id", (req, res) => {
const id = parseInt([Link]);
const initialLength = [Link];
posts = [Link]((p) => [Link] !== id);
if ([Link] < initialLength) {
[Link](204).send(); // 204 No Content
} else {
[Link](404).json({ message: "Пост не найден" });
}
});
В этом примере мы используем простой массив posts для имитации базы данных. В
реальном приложении данные будут храниться в настоящей базе данных.
3.4 Базовая аутентификация (JWT)
Аутентификация — это процесс проверки личности пользователя (кто вы?).
Авторизация — это процесс определения прав доступа пользователя (что вам
разрешено делать?). В этой секции мы рассмотрим базовую аутентификацию с
использованием JSON Web Tokens (JWT).
Что такое JWT?
JWT — это компактный, URL-безопасный способ представления утверждений,
которые должны быть переданы между двумя сторонами. Утверждения в JWT
кодируются как JSON-объект и могут быть цифровой подписью, что позволяет
проверять их целостность и подлинность.
JWT состоит из трех частей, разделенных точками ( . ):
1. Header (Заголовок): Содержит тип токена (JWT) и используемый алгоритм
хеширования (например, HMAC SHA256 или RSA).
2. Payload (Полезная нагрузка): Содержит утверждения (claims) — информацию о
пользователе и другие данные. Например, userId , username , roles .
3. Signature (Подпись): Создается путем хеширования заголовка, полезной
нагрузки и секретного ключа. Подпись используется для проверки того, что токен
не был изменен.
Как работает аутентификация с JWT:
1. Вход пользователя: Пользователь отправляет свои учетные данные (логин/
пароль) на сервер.
2. Генерация токена: Сервер проверяет учетные данные. Если они верны, сервер
генерирует JWT, подписывает его секретным ключом и отправляет обратно
клиенту.
3. Хранение токена: Клиент (браузер) хранит JWT (например, в localStorage или
sessionStorage ).
4. Доступ к защищенным маршрутам: При каждом последующем запросе к
защищенным маршрутам клиент отправляет JWT в заголовке Authorization
(обычно в формате Bearer <token> ).
5. Проверка токена: Сервер перехватывает запрос, извлекает JWT, проверяет его
подпись (используя тот же секретный ключ) и срок действия. Если токен
действителен, сервер разрешает доступ к ресурсу.
Реализация JWT в Express:
Для работы с JWT в [Link] мы будем использовать библиотеку jsonwebtoken .
Установка:
Bash
npm install jsonwebtoken
Пример реализации:
JavaScript
// [Link] (продолжение)
const jwt = require("jsonwebtoken");
const SECRET_KEY = "your_jwt_secret_key"; // В реальном приложении это должен
быть сложный, случайный ключ из переменных окружения!
// Маршрут для входа (логина)
[Link]("/login", (req, res) => {
const { username, password } = [Link];
// В реальном приложении здесь будет проверка пользователя в базе данных
if (username === "user" && password === "password") {
// Генерируем JWT
const token = [Link]({ userId: 1, username: username }, SECRET_KEY, {
expiresIn: "1h" });
[Link]({ message: "Успешный вход", token: token });
} else {
[Link](401).json({ message: "Неверные учетные данные" });
}
});
// Middleware для проверки JWT
const authenticateToken = (req, res, next) => {
const authHeader = [Link]["authorization"];
const token = authHeader && [Link](" ")[1]; // Bearer TOKEN
if (token == null) return [Link](401).json({ message: "Токен не
предоставлен" });
[Link](token, SECRET_KEY, (err, user) => {
if (err) return [Link](403).json({ message: "Недействительный токен"
});
[Link] = user; // Добавляем информацию о пользователе в объект запроса
next();
});
};
// Защищенный маршрут
[Link]("/protected-data", authenticateToken, (req, res) => {
[Link]({ message: `Добро пожаловать, ${[Link]}! Это защищенные
данные.`, user: [Link] });
});
// Пример использования:
// 1. POST запрос на /login с { username: "user", password: "password" }
// 2. Получить токен из ответа
// 3. GET запрос на /protected-data с заголовком Authorization: Bearer
<ваш_токен>
Важные замечания по безопасности JWT:
• Секретный ключ: Никогда не храните секретный ключ в открытом виде в коде.
Используйте переменные окружения (например, через библиотеку dotenv ).
• Срок действия токена: Устанавливайте разумный срок действия токена
( expiresIn ).
• Хранение на клиенте: localStorage уязвим для XSS-атак. Для более безопасного
хранения токенов рассмотрите использование HttpOnly куки.
• Отзыв токенов: JWT по своей природе не могут быть отозваны до истечения
срока действия. Для реализации отзыва (например, при выходе пользователя или
компрометации токена) требуются дополнительные механизмы (например,
"черные списки" токенов на сервере).
3.5 Промежуточный проект: API для блога
Теперь, когда мы изучили [Link], RESTful API и базовую аутентификацию, давайте
расширим наш мини-API и создадим полноценный API для блога. Этот API будет
включать маршруты для управления постами и комментариями, реализуя CRUD-
операции.
Цель: Создать RESTful API для блога с ресурсами /posts и /comments . Пока что
данные будут храниться в памяти (массивах), без использования базы данных.
Шаги:
1. Создайте новый проект Express (или используйте существующий из Главы 1):
Если вы создаете новый проект, повторите шаги из раздела 1.4: mkdir blog-api , cd
blog-api , npm init -y , npm install express jsonwebtoken .
2. Создайте файл [Link] :
3. Запустите сервер:
Убедитесь, что в [Link] есть скрипт start : "start": "node [Link]" .
Запустите сервер:
4. Тестирование API с помощью Postman/Insomnia:
Используйте такие инструменты, как Postman или Insomnia, для отправки HTTP-
запросов к вашему API. Это позволит вам легко тестировать различные маршруты
и методы.
• Вход: Отправьте POST запрос на [Link] с телом JSON:
• Получение постов: Отправьте GET запрос на [Link] .
• Создание поста (защищенный): Отправьте POST запрос на
[Link] с телом JSON:
• Добавление комментария (защищенный): Отправьте POST запрос на
[Link] с телом JSON:
• Удаление поста/комментария (защищенный): Отправьте DELETE запрос на
[Link] (если вы создали пост с ID 3) или
[Link] (если вы создали комментарий с ID 3). Не
забудьте заголовок Authorization .
Этот промежуточный проект является основой для нашего бэкенда. В следующей
главе мы подключим настоящую базу данных, чтобы данные сохранялись постоянно,
а не только в памяти.
3.6 Практические задания по Главе 3
Для закрепления материала и развития практических навыков, выполните
следующие задания. Старайтесь применять все изученные концепции.
1. Написать Express-сервер с ресурсом "products":
• Создайте новый Express-проект.
• Реализуйте ресурс /api/products .
• Добавьте маршруты для:
• GET /api/products : Получить список всех продуктов.
• GET /api/products/:id : Получить продукт по ID.
• POST /api/products : Создать новый продукт (требует аутентификации).
• PUT /api/products/:id : Обновить продукт по ID (требует аутентификации).
• DELETE /api/products/:id : Удалить продукт по ID (требует аутентификации).
• Используйте массив в памяти для хранения данных о продуктах (например, { id,
name, price } ).
• Реализуйте базовую JWT-аутентификацию, как показано в главе, с маршрутом
/api/auth/login .
• Убедитесь, что маршруты POST , PUT , DELETE защищены middleware
authenticateToken .
2. Провести запросы к API через Postman/Insomnia:
• Запустите ваш Express-сервер из задания 1.
• Используйте Postman или Insomnia для выполнения следующих запросов:
• GET /api/products (без токена).
• POST /api/auth/login с тестовыми учетными данными для получения токена.
• POST /api/products с токеном в заголовке Authorization и телом JSON для
создания нового продукта.
• GET /api/products/:id для получения созданного продукта.
• PUT /api/products/:id для обновления продукта (с токеном).
• DELETE /api/products/:id для удаления продукта (с токеном).
• Попробуйте выполнить POST , PUT , DELETE запросы без токена и
убедитесь, что сервер возвращает ошибку 401/403.
3. Добавить валидацию входных данных и отлов ошибок:
• В проекте из задания 1, для маршрута POST /api/products , добавьте валидацию
входных данных.
• Убедитесь, что name и price присутствуют в теле запроса и price является
числом.
• Если данные невалидны, возвращайте статус 400 (Bad Request) с
соответствующим сообщением об ошибке.
• Реализуйте глобальный обработчик ошибок (middleware с 4 аргументами),
который будет перехватывать все необработанные ошибки в приложении и
возвращать статус 500 (Internal Server Error) с общим сообщением.
• Протестируйте валидацию, отправляя запросы с неполными или
некорректными данными.
4. Реализовать простую регистрацию/логин с JWT:
• Расширьте ваш API из задания 1, добавив маршрут POST /api/auth/register .
• Этот маршрут должен принимать username и password .
• Проверяйте, что username не занят (в вашем массиве пользователей в памяти).
• Если username свободен, создайте нового пользователя, добавьте его в массив
users и верните сообщение об успешной регистрации.
• Бонус: Для паролей используйте библиотеку bcryptjs для хеширования
паролей перед сохранением и сравнения при логине. Это критически важно
для безопасности.
• Установите npm install bcryptjs .
• При регистрации хешируйте пароль: const hashedPassword = await
[Link](password, 10);
• При логине сравнивайте: const isMatch = await [Link](password,
[Link]);
Глава 4: Работа с базами данных (SQL и NoSQL)
В предыдущих главах мы научились создавать фронтенд на React и бэкенд на
[Link]. Однако данные в нашем API для блога пока что хранятся только в памяти,
что означает их потерю при перезапуске сервера. В реальных приложениях данные
должны быть постоянными и храниться в базах данных. В этой главе мы изучим два
основных подхода к работе с базами данных: реляционные (SQL) и нереляционные
(NoSQL), поймем их отличия и сценарии использования.
Мы научимся взаимодействовать с популярными базами данных: PostgreSQL (пример
SQL-базы) и MongoDB (пример NoSQL-базы). Для упрощения работы с базами данных в
[Link] мы будем использовать ORM (Object-Relational Mapper) для SQL-баз, такие как
Sequelize или Prisma, и ODM (Object-Document Mapper) для MongoDB, такой как
Mongoose. Эти инструменты позволяют работать с данными, используя JavaScript-
объекты, вместо написания "сырых" SQL-запросов или низкоуровневых операций с
документами.
Мы разработаем схемы данных (модели), научимся выполнять типовые операции
CRUD (Create, Read, Update, Delete), а также рассмотрим более продвинутые
концепции, такие как индексы для оптимизации запросов и миграции для
управления версиями схемы базы данных. Понимание того, как эффективно работать
с базами данных, является критически важным навыком для любого Full-Stack
разработчика.
4.1 Моделирование данных (сущности, связи)
Прежде чем начать работать с базами данных, важно понять, как моделировать
данные. Моделирование данных — это процесс создания абстрактной модели данных,
которая определяет, как данные будут храниться, обрабатываться и использоваться в
информационной системе. Это включает определение сущностей (объектов), их
атрибутов (свойств) и связей между ними.
4.1.1 Реляционные базы данных (SQL)
Реляционные базы данных (RDBMS) хранят данные в таблицах, которые состоят из
строк и столбцов. Каждая таблица представляет собой сущность (например, Users ,
Posts , Comments ). Столбцы таблицы представляют атрибуты сущности (например,
name , email для Users ).
Ключевые концепции:
• Таблица (Table): Коллекция связанных данных, организованных в строки и
столбцы.
• Строка (Row) / Запись (Record) / Кортеж (Tuple): Отдельный набор данных в
таблице.
• Столбец (Column) / Поле (Field) / Атрибут (Attribute): Отдельный элемент данных
в таблице, имеющий определенный тип данных.
• Первичный ключ (Primary Key): Уникальный идентификатор для каждой строки
в таблице. Гарантирует уникальность и позволяет быстро находить записи.
• Внешний ключ (Foreign Key): Столбец (или набор столбцов) в одной таблице,
который ссылается на первичный ключ в другой таблице. Используется для
установления связей между таблицами.
Типы связей:
• Один-ко-многим (One-to-Many): Одна запись в таблице A может быть связана с
несколькими записями в таблице B, но каждая запись в таблице B связана только
с одной записью в таблице A. Например, один пользователь может иметь много
постов, но каждый пост принадлежит только одному пользователю.
• Реализация: В таблице "многие" (Posts) добавляется внешний ключ,
ссылающийся на первичный ключ таблицы "один" (Users).
• Многие-ко-многим (Many-to-Many): Одна запись в таблице A может быть связана
с несколькими записями в таблице B, и одна запись в таблице B может быть
связана с несколькими записями в таблице A. Например, один пост может иметь
много тегов, и один тег может быть применен ко многим постам.
• Реализация: Создается промежуточная (связующая) таблица, которая содержит
внешние ключи, ссылающиеся на первичные ключи обеих таблиц.
• Один-к-одному (One-to-One): Одна запись в таблице A связана только с одной
записью в таблице B, и наоборот. Встречается реже, часто используется для
разделения больших таблиц или хранения конфиденциальных данных.
• Реализация: В одной из таблиц добавляется внешний ключ, который также
является уникальным.
Пример моделирования для блога (SQL):
• Users таблица:
• id (PRIMARY KEY)
• username
• email
• password_hash
• Posts таблица:
• id (PRIMARY KEY)
• title
• content
• author_id (FOREIGN KEY -> [Link])
• created_at
• Comments таблица:
• id (PRIMARY KEY)
• post_id (FOREIGN KEY -> [Link])
• author_id (FOREIGN KEY -> [Link])
• text
• created_at
4.1.2 Нереляционные базы данных (NoSQL)
Нереляционные базы данных (NoSQL) предлагают более гибкие модели данных,
которые не требуют фиксированной схемы. Они часто используются для больших
объемов неструктурированных или полуструктурированных данных, а также для
приложений, требующих высокой масштабируемости и доступности.
Типы NoSQL баз данных:
• Документоориентированные (Document-oriented): Хранят данные в виде
документов (обычно JSON или BSON), которые могут иметь вложенные структуры.
Примеры: MongoDB, Couchbase.
• Ключ-значение (Key-Value): Простейшая модель, где данные хранятся как пары
ключ-значение. Примеры: Redis, DynamoDB.
• Колоночные (Column-family): Хранят данные в столбцах, оптимизированы для
агрегации данных по столбцам. Примеры: Cassandra, HBase.
• Графовые (Graph): Хранят данные в виде узлов и ребер, оптимизированы для
работы со связями между данными. Примеры: Neo4j, Amazon Neptune.
Пример моделирования для блога (MongoDB - документоориентированная):
В MongoDB данные хранятся в коллекциях, которые содержат документы. Документы
могут быть вложенными, что позволяет хранить связанные данные в одном
документе.
• users коллекция:
• posts коллекция:
В MongoDB вы можете выбирать между встраиванием (embedding) связанных данных
в один документ (как comments в posts выше) или ссылками (referencing) на другие
документы. Выбор зависит от характера данных и паттернов доступа.
4.2 Работа с MongoDB/Mongoose
MongoDB — это популярная документоориентированная NoSQL база данных. Она
хранит данные в гибких, JSON-подобных документах, что позволяет легко
адаптироваться к изменяющимся требованиям приложения. Mongoose — это ODM
(Object-Document Mapper) для [Link] и MongoDB. Он предоставляет объектно-
ориентированное решение для моделирования данных, включая схемы, валидацию и
удобные методы для взаимодействия с базой данных.
4.2.1 Установка MongoDB и Mongoose
Установка MongoDB:
Вы можете установить MongoDB локально (см. официальную документацию для
вашей ОС: [Link] или использовать облачный
сервис, такой как MongoDB Atlas (рекомендуется для простоты и масштабируемости:
[Link] MongoDB Atlas предоставляет бесплатный "M0"
кластер, которого достаточно для обучения и небольших проектов.
Установка Mongoose:
Bash
npm install mongoose
4.2.2 Подключение к MongoDB и определение схем
Для подключения к MongoDB с помощью Mongoose:
JavaScript
// [Link]
const mongoose = require("mongoose");
const connectDB = async () => {
try {
const conn = await [Link]([Link].MONGO_URI ||
"mongodb://localhost:27017/blogdb", {
useNewUrlParser: true,
useUnifiedTopology: true,
// useCreateIndex: true, // Deprecated in Mongoose 6.0+
// useFindAndModify: false // Deprecated in Mongoose 6.0+
});
[Link](`MongoDB подключена: ${[Link]}`);
} catch (error) {
[Link](`Ошибка подключения к MongoDB: ${[Link]}`);
[Link](1); // Выход из процесса с ошибкой
}
};
[Link] = connectDB;
Определение схем (Schemas):
Схемы Mongoose определяют структуру документов в коллекции, включая типы
данных, валидацию и связи. Модели Mongoose — это конструкторы, с помощью
которых вы создаете экземпляры документов.
models/[Link] :
JavaScript
const mongoose = require("mongoose");
const userSchema = new [Link]({
username: {
type: String,
required: true,
unique: true,
trim: true,
},
email: {
type: String,
required: true,
unique: true,
trim: true,
lowercase: true,
match: [/\S+@\S+\.\S+/, "Некорректный формат email"],
},
password: {
type: String,
required: true,
},
createdAt: {
type: Date,
default: [Link],
},
});
[Link] = [Link]("User", userSchema);
models/[Link] :
JavaScript
const mongoose = require("mongoose");
const postSchema = new [Link]({
title: {
type: String,
required: true,
trim: true,
},
content: {
type: String,
required: true,
},
author: {
type: [Link], // Ссылка на ID пользователя
ref: "User", // Ссылка на модель User
required: true,
},
comments: [
{
text: {
type: String,
required: true,
},
author: {
type: [Link],
ref: "User",
required: true,
},
createdAt: {
type: Date,
default: [Link],
},
},
],
createdAt: {
type: Date,
default: [Link],
},
});
[Link] = [Link]("Post", postSchema);
4.2.3 CRUD-операции с Mongoose
После определения моделей вы можете использовать их для выполнения CRUD-
операций.
Создание (Create):
JavaScript
const User = require("../models/User");
const createUser = async (username, email, password) => {
try {
const newUser = new User({
username,
email,
password, // В реальном приложении здесь будет хешированный пароль
});
const savedUser = await [Link]();
[Link]("Пользователь создан:", savedUser);
return savedUser;
} catch (error) {
[Link]("Ошибка при создании пользователя:", [Link]);
throw error;
}
};
// Пример использования:
// createUser("testuser", "test@[Link]", "password123");
Чтение (Read):
JavaScript
const User = require("../models/User");
const Post = require("../models/Post");
// Найти всех пользователей
const findAllUsers = async () => {
try {
const users = await [Link]();
[Link]("Все пользователи:", users);
return users;
} catch (error) {
[Link]("Ошибка при поиске пользователей:", [Link]);
throw error;
}
};
// Найти пользователя по ID
const findUserById = async (id) => {
try {
const user = await [Link](id);
[Link]("Пользователь по ID:", user);
return user;
} catch (error) {
[Link]("Ошибка при поиске пользователя по ID:", [Link]);
throw error;
}
};
// Найти посты и "заполнить" информацию об авторе (populate)
const findPostsWithAuthors = async () => {
try {
const posts = await [Link]().populate("author", "username email"); //
Заполняем поле author, выбирая username и email
[Link]("Посты с авторами:", posts);
return posts;
} catch (error) {
[Link]("Ошибка при поиске постов:", [Link]);
throw error;
}
};
// Пример использования:
// findAllUsers();
// findUserById("60c72b2f9b1e8c001c8e4d1a"); // Замените на реальный ID
// findPostsWithAuthors();
Обновление (Update):
JavaScript
const User = require("../models/User");
const updateUser = async (id, updates) => {
try {
const updatedUser = await [Link](id, updates, { new: true
}); // new: true возвращает обновленный документ
[Link]("Пользователь обновлен:", updatedUser);
return updatedUser;
} catch (error) {
[Link]("Ошибка при обновлении пользователя:", [Link]);
throw error;
}
};
// Пример использования:
// updateUser("60c72b2f9b1e8c001c8e4d1a", { email: "new_email@[Link]"
});
Удаление (Delete):
JavaScript
const User = require("../models/User");
const deleteUser = async (id) => {
try {
const deletedUser = await [Link](id);
[Link]("Пользователь удален:", deletedUser);
return deletedUser;
} catch (error) {
[Link]("Ошибка при удалении пользователя:", [Link]);
throw error;
}
};
// Пример использования:
// deleteUser("60c72b2f9b1e8c001c8e4d1a");
4.3 Работа с SQL (PostgreSQL) и ORM (Sequelize/Prisma)
PostgreSQL — это мощная, открытая, объектно-реляционная система управления
базами данных (ОРСУБД), известная своей надежностью, стабильностью и
производительностью. Она поддерживает широкий спектр функций SQL и является
отличным выбором для большинства веб-приложений.
ORM (Object-Relational Mapper) — это инструмент, который позволяет
взаимодействовать с реляционной базой данных, используя объектно-
ориентированный подход. Вместо написания SQL-запросов вы работаете с JavaScript-
объектами, которые отображаются на таблицы и строки в базе данных. Это упрощает
разработку, делает код более читаемым и переносимым между различными СУБД.
Мы рассмотрим Sequelize как пример ORM. Prisma — это более современный ORM
нового поколения, который предлагает улучшенный опыт разработчика, но для
начала Sequelize является хорошим выбором.
4.3.1 Установка PostgreSQL и Sequelize
Установка PostgreSQL:
Вы можете установить PostgreSQL локально (см. официальную документацию:
[Link] или использовать облачный сервис, такой как
ElephantSQL (для небольших проектов: [Link] или облачные
решения от AWS, Google Cloud, Azure.
После установки создайте новую базу данных и пользователя для вашего
приложения.
Установка Sequelize и драйвера PostgreSQL:
Bash
npm install sequelize pg pg-hstore
• sequelize : Сам ORM.
• pg : Драйвер для PostgreSQL.
• pg-hstore : Для поддержки типа данных hstore в PostgreSQL (не всегда нужен, но
часто устанавливается вместе с pg ).
4.3.2 Подключение к PostgreSQL и определение моделей
Для подключения к PostgreSQL с помощью Sequelize:
JavaScript
// config/[Link]
const { Sequelize } = require("sequelize");
const sequelize = new Sequelize(
[Link].DB_NAME || "blogdb",
[Link].DB_USER || "postgres",
[Link].DB_PASSWORD || "password",
{
host: [Link].DB_HOST || "localhost",
dialect: "postgres",
logging: false, // Отключить логирование SQL-запросов в консоль
}
);
const connectDB = async () => {
try {
await [Link]();
[Link]("Подключение к PostgreSQL успешно установлено.");
} catch (error) {
[Link]("Не удалось подключиться к PostgreSQL:", error);
[Link](1);
}
};
[Link] = { sequelize, connectDB };
Определение моделей (Models):
Модели Sequelize определяют таблицы в базе данных и их столбцы, а также связи
между ними.
models/[Link] :
JavaScript
const { DataTypes } = require("sequelize");
const { sequelize } = require("../config/database");
const User = [Link]("User", {
id: {
type: [Link],
autoIncrement: true,
primaryKey: true,
},
username: {
type: [Link],
allowNull: false,
unique: true,
},
email: {
type: [Link],
allowNull: false,
unique: true,
validate: {
isEmail: true,
},
},
password: {
type: [Link],
allowNull: false,
},
});
[Link] = User;
models/[Link] :
JavaScript
const { DataTypes } = require("sequelize");
const { sequelize } = require("../config/database");
const User = require("./User"); // Импортируем модель User
const Post = [Link]("Post", {
id: {
type: [Link],
autoIncrement: true,
primaryKey: true,
},
title: {
type: [Link],
allowNull: false,
},
content: {
type: [Link],
allowNull: false,
},
// authorId будет добавлен автоматически Sequelize при определении связи
});
// Определение связи: Один-ко-многим (User имеет много Posts)
[Link](Post, { foreignKey: "authorId" });
[Link](User, { foreignKey: "authorId" });
[Link] = Post;
models/[Link] :
JavaScript
const { DataTypes } = require("sequelize");
const { sequelize } = require("../config/database");
const User = require("./User");
const Post = require("./Post");
const Comment = [Link]("Comment", {
id: {
type: [Link],
autoIncrement: true,
primaryKey: true,
},
text: {
type: [Link],
allowNull: false,
},
// postId и authorId будут добавлены автоматически
});
// Определение связей
[Link](Comment, { foreignKey: "postId" });
[Link](Post, { foreignKey: "postId" });
[Link](Comment, { foreignKey: "authorId" });
[Link](User, { foreignKey: "authorId" });
[Link] = Comment;
4.3.3 CRUD-операции с Sequelize
После определения моделей вы можете использовать их для выполнения CRUD-
операций.
Синхронизация моделей с базой данных (создание таблиц):
JavaScript
const { sequelize, connectDB } = require("./config/database");
require("./models/User"); // Импортируем модели, чтобы они были определены
require("./models/Post");
require("./models/Comment");
const syncDatabase = async () => {
await connectDB(); // Сначала подключаемся
try {
// { force: true } удалит существующие таблицы и создаст их заново.
Использовать осторожно!
// { alter: true } попытается изменить существующие таблицы, чтобы они
соответствовали моделям.
await [Link]({ alter: true });
[Link]("Все модели синхронизированы с базой данных.");
} catch (error) {
[Link]("Ошибка синхронизации базы данных:", error);
}
};
// syncDatabase(); // Вызовите эту функцию один раз при запуске приложения
или в скрипте миграции
Создание (Create):
JavaScript
const User = require("../models/User");
const Post = require("../models/Post");
const createNewUser = async (username, email, password) => {
try {
const user = await [Link]({ username, email, password });
[Link]("Пользователь создан:", [Link]());
return user;
} catch (error) {
[Link]("Ошибка при создании пользователя:", [Link]);
throw error;
}
};
const createNewPost = async (title, content, authorId) => {
try {
const post = await [Link]({ title, content, authorId });
[Link]("Пост создан:", [Link]());
return post;
} catch (error) {
[Link]("Ошибка при создании поста:", [Link]);
throw error;
}
};
// Пример использования:
// (async () => {
// await syncDatabase();
// const user = await createNewUser("john_doe", "john@[Link]",
"hashed_password");
// if (user) {
// await createNewPost("Мой первый пост", "Содержимое поста", [Link]);
// }
// })();
Чтение (Read):
JavaScript
const User = require("../models/User");
const Post = require("../models/Post");
const Comment = require("../models/Comment");
// Найти всех пользователей
const getAllUsers = async () => {
try {
const users = await [Link]();
[Link]("Все пользователи:", [Link](u => [Link]()));
return users;
} catch (error) {
[Link]("Ошибка при получении пользователей:", [Link]);
throw error;
}
};
// Найти пост по ID с автором и комментариями
const getPostByIdWithRelations = async (postId) => {
try {
const post = await [Link](postId, {
include: [
{ model: User, as: "User", attributes: ["username", "email"] }, //
Включаем автора
{ model: Comment, as: "Comments", include: [{ model: User, as:
"User", attributes: ["username"] }] } // Включаем комментарии с авторами
],
});
[Link]("Пост с отношениями:", post ? [Link]() : null);
return post;
} catch (error) {
[Link]("Ошибка при получении поста:", [Link]);
throw error;
}
};
// Пример использования:
// (async () => {
// await syncDatabase();
// await getAllUsers();
// await getPostByIdWithRelations(1); // Замените на реальный ID поста
// })();
Обновление (Update):
JavaScript
const Post = require("../models/Post");
const updatePost = async (postId, updates) => {
try {
const [affectedRows] = await [Link](updates, {
where: { id: postId },
});
if (affectedRows > 0) {
const updatedPost = await [Link](postId);
[Link]("Пост обновлен:", [Link]());
return updatedPost;
} else {
[Link]("Пост не найден или нет изменений.");
return null;
}
} catch (error) {
[Link]("Ошибка при обновлении поста:", [Link]);
throw error;
}
};
// Пример использования:
// (async () => {
// await syncDatabase();
// await updatePost(1, { title: "Обновленный заголовок поста" });
// })();
Удаление (Delete):
JavaScript
const Post = require("../models/Post");
const deletePost = async (postId) => {
try {
const deletedRows = await [Link]({
where: { id: postId },
});
if (deletedRows > 0) {
[Link](`Пост с ID ${postId} успешно удален.`);
return true;
} else {
[Link]("Пост не найден.");
return false;
}
} catch (error) {
[Link]("Ошибка при удалении поста:", [Link]);
throw error;
}
};
// Пример использования:
// (async () => {
// await syncDatabase();
// await deletePost(1);
// })();
4.4 Миграции, индексы, транзакции
4.4.1 Миграции (Migrations)
Миграции — это способ управления изменениями в схеме базы данных с течением
времени. По мере развития приложения вам, вероятно, потребуется добавлять новые
таблицы, изменять существующие столбцы или создавать индексы. Миграции
позволяют отслеживать эти изменения в коде, применять их к базе данных и
откатывать при необходимости. Это особенно важно в командной разработке, где у
разных разработчиков могут быть разные версии схемы базы данных.
Для Sequelize миграции обычно управляются с помощью пакета sequelize-cli .
Установка sequelize-cli :
Bash
npm install -g sequelize-cli
Инициализация проекта Sequelize:
Bash
sequelize init
Эта команда создаст папки config , migrations , models , seeders .
Создание миграции:
Bash
sequelize migration:generate --name create-users-table
Эта команда создаст новый файл миграции в папке migrations .
Пример файла миграции ( <timestamp>-[Link] ):
JavaScript
"use strict";
[Link] = {
up: async (queryInterface, Sequelize) => {
await [Link]("Users", {
id: {
allowNull: false,
autoIncrement: true,
primaryKey: true,
type: [Link],
},
username: {
type: [Link],
allowNull: false,
unique: true,
},
email: {
type: [Link],
allowNull: false,
unique: true,
},
password: {
type: [Link],
allowNull: false,
},
createdAt: {
allowNull: false,
type: [Link],
},
updatedAt: {
allowNull: false,
type: [Link],
},
});
},
down: async (queryInterface, Sequelize) => {
await [Link]("Users");
},
};
• up : Определяет изменения, которые нужно применить (например, создать
таблицу).
• down : Определяет изменения, которые нужно откатить (например, удалить
таблицу).
Выполнение миграций:
Bash
sequelize db:migrate
Откат миграций:
Bash
sequelize db:migrate:undo
Миграции обеспечивают контролируемый и версионированный подход к управлению
схемой базы данных, что является стандартом в профессиональной разработке.
4.4.2 Индексы (Indexes)
Индексы — это специальные структуры данных, которые улучшают скорость
операций поиска данных в базе данных. Они работают аналогично индексу в книге:
вместо того чтобы просматривать каждую страницу, вы используете индекс, чтобы
быстро найти нужную информацию. Индексы создаются на одном или нескольких
столбцах таблицы.
Когда использовать индексы:
• На столбцах, которые часто используются в условиях WHERE (для фильтрации).
• На столбцах, используемых в JOIN операциях (для связывания таблиц).
• На столбцах, используемых для сортировки ( ORDER BY ).
• На столбцах, которые должны быть уникальными (например, email , username ).
Пример создания индекса в Sequelize (в определении модели):
JavaScript
const User = [Link]("User", {
// ... другие поля
email: {
type: [Link],
allowNull: false,
unique: true, // Это автоматически создаст уникальный индекс
},
}, {
indexes: [
{ // Дополнительный индекс для быстрого поиска по username
unique: true,
fields: ["username"]
},
{ // Индекс для быстрого поиска по email (если не unique)
fields: ["email"]
}
]
});
Важно: Индексы улучшают скорость чтения, но замедляют операции записи (INSERT,
UPDATE, DELETE), так как индекс также должен быть обновлен. Поэтому не следует
индексировать каждый столбец; индексируйте только те, которые действительно
нуждаются в оптимизации поиска.
4.4.3 Транзакции (Transactions)
Транзакция — это последовательность операций, которые выполняются как единое,
атомарное целое. Это означает, что либо все операции в транзакции успешно
завершаются (коммит), либо ни одна из них не применяется (откат). Транзакции
гарантируют целостность данных, особенно когда несколько операций должны быть
выполнены вместе, и сбой одной из них должен привести к отмене всех остальных.
Свойства ACID транзакций:
• Атомарность (Atomicity): Все или ничего. Либо все операции транзакции
выполняются успешно, либо ни одна из них.
• Согласованность (Consistency): Транзакция переводит базу данных из одного
согласованного состояния в другое.
• Изолированность (Isolation): Параллельно выполняющиеся транзакции не
влияют друг на друга.
• Долговечность (Durability): После успешного завершения транзакции (коммита)
изменения сохраняются в базе данных навсегда, даже в случае сбоя системы.
Пример использования транзакций в Sequelize:
Представьте, что вы переводите деньги с одного счета на другой. Это две операции:
списание с одного счета и зачисление на другой. Если одна из них не удастся, обе
должны быть отменены.
JavaScript
const { sequelize } = require("../config/database");
const User = require("../models/User");
const transferFunds = async (senderId, receiverId, amount) => {
const t = await [Link](); // Начинаем транзакцию
try {
const sender = await [Link](senderId, { transaction: t });
const receiver = await [Link](receiverId, { transaction: t });
if (!sender || !receiver) {
throw new Error("Отправитель или получатель не найден.");
}
if ([Link] < amount) {
throw new Error("Недостаточно средств.");
}
await [Link]({ balance: [Link] - amount }, { transaction:
t });
await [Link]({ balance: [Link] + amount }, {
transaction: t });
await [Link](); // Если все успешно, фиксируем изменения
[Link]("Перевод средств успешно выполнен.");
} catch (error) {
await [Link](); // Если произошла ошибка, откатываем все изменения
[Link]("Ошибка при переводе средств:", [Link]);
throw error;
}
};
// Пример использования (предполагается, что у User есть поле balance)
// (async () => {
// await syncDatabase();
// // Создайте тестовых пользователей с балансом
// await transferFunds(1, 2, 50);
// })();
Транзакции являются критически важными для обеспечения целостности данных в
приложениях, особенно в финансовых или других чувствительных к данным
системах.
4.5 Промежуточный проект: Расширение API блога с БД
Теперь, когда мы изучили работу с базами данных (MongoDB/Mongoose и
PostgreSQL/Sequelize), давайте обновим наш API для блога, чтобы он использовал
настоящую базу данных вместо хранения данных в памяти. Вы можете выбрать любую
из двух баз данных, которую вы предпочитаете или которая лучше подходит для
вашего проекта.
Цель: Интегрировать выбранную базу данных (MongoDB или PostgreSQL) в API блога,
заменив массивы в памяти на операции с БД.
Шаги (для MongoDB с Mongoose):
1. Настройка проекта:
• Убедитесь, что у вас установлен MongoDB (локально или через MongoDB Atlas).
• В вашем проекте API блога ( blog-api из Главы 3) установите mongoose :
• Установите dotenv для управления переменными окружения:
• Создайте файл .env в корне проекта и добавьте строку подключения к вашей
MongoDB:
2. Создайте папку config и файл [Link] :
config/[Link] :
3. Создайте папку models и определите схемы:
Создайте models/[Link] и models/[Link] (с вложенными комментариями или
отдельной моделью Comment , если хотите) на основе примеров из раздела 4.2.2.
4. Обновите [Link] :
• В начале [Link] импортируйте и вызовите функцию подключения к БД:
• Замените все операции с массивами posts , comments , users на
соответствующие операции с моделями Mongoose ( [Link]() , [Link]() ,
[Link]() и т.д.).
• Важно: Для аутентификации, при регистрации и логине, используйте bcryptjs
для хеширования паролей. Пример использования bcryptjs был в задании 4
Главы 3.
Пример обновления маршрутов (для постов):
JavaScript
// ... в [Link]
// GET /api/posts - Получить все посты
[Link]("/api/posts", async (req, res) => {
try {
const posts = await [Link]().populate("author", "username"); //
Заполняем автора
[Link](posts);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// GET /api/posts/:id - Получить пост по ID
[Link]("/api/posts/:id", async (req, res) => {
try {
const post = await [Link]([Link]).populate("author",
"username");
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
[Link](post);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// POST /api/posts - Создать новый пост (защищенный маршрут)
[Link]("/api/posts", authenticateToken, async (req, res) => {
const { title, content } = [Link];
if (!title || !content) {
return [Link](400).json({ message: "Заголовок и содержимое
обязательны" });
}
try {
const newPost = new Post({
title,
content,
author: [Link], // ID пользователя из JWT
});
const savedPost = await [Link]();
// После сохранения, возможно, захотим вернуть пост с заполненным автором
const populatedPost = await
[Link](savedPost._id).populate("author", "username");
[Link](201).json(populatedPost);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// PUT /api/posts/:id - Обновить пост по ID (защищенный маршрут)
[Link]("/api/posts/:id", authenticateToken, async (req, res) => {
try {
const post = await [Link]([Link]);
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
// Проверка, является ли пользователь автором поста
if ([Link]() !== [Link]) {
return [Link](403).json({ message: "У вас нет прав для
редактирования этого поста" });
}
[Link] = [Link] || [Link];
[Link] = [Link] || [Link];
const updatedPost = await [Link]();
[Link](updatedPost);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// DELETE /api/posts/:id - Удалить пост по ID (защищенный маршрут)
[Link]("/api/posts/:id", authenticateToken, async (req, res) => {
try {
const post = await [Link]([Link]);
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
// Проверка, является ли пользователь автором поста
if ([Link]() !== [Link]) {
return [Link](403).json({ message: "У вас нет прав для удаления
этого поста" });
}
await [Link](); // Используйте deleteOne() или deleteMany() в
Mongoose 6+
[Link](204).send();
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// ... аналогично обновите маршруты для комментариев и аутентификации
Шаги (для PostgreSQL с Sequelize):
1. Настройка проекта:
• Убедитесь, что у вас установлен PostgreSQL и создана база данных.
• В вашем проекте API блога установите sequelize , pg , pg-hstore и dotenv :
• Создайте файл .env в корне проекта и добавьте данные для подключения к
вашей PostgreSQL:
2. Создайте папку config и файл [Link] :
config/[Link] :
3. Создайте папку models и определите модели:
Создайте models/[Link] , models/[Link] , models/[Link] на основе примеров из
раздела 4.3.2. Убедитесь, что связи между моделями определены.
4. Обновите [Link] :
• В начале [Link] импортируйте и вызовите функцию подключения к БД и
синхронизации моделей:
• Замените все операции с массивами posts , comments , users на
соответствующие операции с моделями Sequelize ( [Link]() , [Link]() ,
[Link]() и т.д.).
• Важно: Для аутентификации, при регистрации и логине, используйте bcryptjs
для хеширования паролей.
Пример обновления маршрутов (для постов):
JavaScript
// ... в [Link]
// GET /api/posts - Получить все посты
[Link]("/api/posts", async (req, res) => {
try {
const posts = await [Link]({
include: [{ model: User, as: "User", attributes: ["username"] }],
});
[Link](posts);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// GET /api/posts/:id - Получить пост по ID
[Link]("/api/posts/:id", async (req, res) => {
try {
const post = await [Link]([Link], {
include: [{ model: User, as: "User", attributes: ["username"] }],
});
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
[Link](post);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// POST /api/posts - Создать новый пост (защищенный маршрут)
[Link]("/api/posts", authenticateToken, async (req, res) => {
const { title, content } = [Link];
if (!title || !content) {
return [Link](400).json({ message: "Заголовок и содержимое
обязательны" });
}
try {
const newPost = await [Link]({
title,
content,
authorId: [Link], // ID пользователя из JWT
});
const populatedPost = await [Link]([Link], {
include: [{ model: User, as: "User", attributes: ["username"] }],
});
[Link](201).json(populatedPost);
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// PUT /api/posts/:id - Обновить пост по ID (защищенный маршрут)
[Link]("/api/posts/:id", authenticateToken, async (req, res) => {
try {
const post = await [Link]([Link]);
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
// Проверка, является ли пользователь автором поста
if ([Link] !== [Link]) {
return [Link](403).json({ message: "У вас нет прав для
редактирования этого поста" });
}
const [affectedRows] = await [Link](
{ title: [Link], content: [Link] },
{ where: { id: [Link] } }
);
if (affectedRows > 0) {
const updatedPost = await [Link]([Link]);
[Link](updatedPost);
} else {
[Link](400).json({ message: "Не удалось обновить пост" });
}
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// DELETE /api/posts/:id - Удалить пост по ID (защищенный маршрут)
[Link]("/api/posts/:id", authenticateToken, async (req, res) => {
try {
const post = await [Link]([Link]);
if (!post) {
return [Link](404).json({ message: "Пост не найден" });
}
// Проверка, является ли пользователь автором поста
if ([Link] !== [Link]) {
return [Link](403).json({ message: "У вас нет прав для удаления
этого поста" });
}
const deletedRows = await [Link]({ where: { id: [Link] } });
if (deletedRows > 0) {
[Link](204).send();
} else {
[Link](400).json({ message: "Не удалось удалить пост" });
}
} catch (error) {
[Link](500).json({ message: [Link] });
}
});
// ... аналогично обновите маршруты для комментариев и аутентификации
Тестирование:
После обновления API, запустите сервер и протестируйте все CRUD-операции с
помощью Postman/Insomnia, убедившись, что данные теперь сохраняются в вашей
базе данных.
4.6 Практические задания по Главе 4
Для закрепления материала и развития практических навыков, выполните
следующие задания. Выберите одну из баз данных (MongoDB или PostgreSQL) для
выполнения заданий.
1. Спроектировать модель "Product" и "Category":
• Для MongoDB (Mongoose):
• Определите схему Category с полями name (строка, уникальное) и
description (строка).
• Определите схему Product с полями name (строка, уникальное), description
(строка), price (число, обязательное), category (ссылка на Category ).
• Реализуйте вложенные комментарии к продуктам (как в примере с постами).
• Для PostgreSQL (Sequelize):
• Определите модель Category с полями id , name (строка, уникальное) и
description (строка).
• Определите модель Product с полями id , name (строка, уникальное),
description (строка), price (число, обязательное).
• Установите связь "один-ко-многим" между Category и Product (одна
категория может иметь много продуктов).
• Определите модель Comment для продуктов и установите связи с Product и
User .
2. Реализовать CRUD-операции для "Product" и "Category":
• Используя выбранную базу данных и ORM/ODM, реализуйте функции для:
• Создания новой категории.
• Получения всех категорий.
• Создания нового продукта (с указанием категории).
• Получения всех продуктов (с "заполнением" информации о категории).
• Получения продукта по ID (с "заполнением" информации о категории и
комментариях).
• Обновления продукта.
• Удаления продукта.
• Добавления комментария к продукту.
• Удаления комментария к продукту.
3. Интегрировать БД в API из Главы 3:
• Возьмите ваш API для блога из Главы 3.
• Замените все операции с массивами в памяти на операции с вашей выбранной
базой данных (MongoDB или PostgreSQL), используя созданные модели.
• Убедитесь, что аутентификация и авторизация по-прежнему работают
корректно, и что данные теперь сохраняются в базе данных.
• Протестируйте все маршруты с помощью Postman/Insomnia.
4. Использовать простую миграцию (для PostgreSQL) или индексацию (для
MongoDB):
• Для PostgreSQL:
• Используйте sequelize-cli для создания миграции, которая добавляет новую
таблицу (например, Tags ) или новый столбец в существующую таблицу
(например, rating в Product ).
• Выполните миграцию и убедитесь, что схема базы данных обновилась.
• (Опционально) Откатите миграцию и убедитесь, что изменения отменились.
• Для MongoDB:
• Добавьте индексы к полям, которые часто используются для поиска,
например, name в Product или title в Post .
• Убедитесь, что поле email в модели User имеет уникальный индекс.
5. Реализовать транзакцию (для PostgreSQL):
• Для PostgreSQL:
• Создайте функцию, которая имитирует покупку продукта: она должна
уменьшать количество продукта на складе и увеличивать количество
купленных продуктов у пользователя. Эти две операции должны быть
выполнены в одной транзакции. Если одна из них не удается, обе должны
быть отменены.
• Протестируйте сценарий, когда транзакция должна быть откатана
(например, если продукта нет в наличии).
Глава 5: Тестирование приложений
Разработка программного обеспечения — это не только написание кода, но и
обеспечение его качества и надежности. Тестирование является неотъемлемой
частью этого процесса. Оно помогает выявлять ошибки на ранних стадиях
разработки, гарантировать, что приложение работает так, как ожидается, и
предотвращать регрессии (появление старых ошибок после внесения новых
изменений). В этой главе мы погрузимся в мир тестирования JavaScript-приложений,
охватывая как фронтенд, так и бэкенд.
Мы рассмотрим различные типы тестирования: модульное (unit testing),
интеграционное (integration testing) и сквозное (end-to-end testing). Для каждого типа
мы изучим популярные инструменты и фреймворки, такие как Jest для модульного и
интеграционного тестирования, и Playwright/Cypress для сквозного тестирования. Мы
научимся писать эффективные тесты, использовать моки (mocks) и стабы (stubs) для
изоляции компонентов, а также интегрировать тестирование в процесс разработки.
Понимание принципов тестирования и умение писать качественные тесты — это
ключевой навык для любого профессионального разработчика, который стремится
создавать надежные и поддерживаемые приложения.
5.1 Типы тестирования: модульное, интеграционное, сквозное
Прежде чем приступить к написанию тестов, важно понять, какие типы тестирования
существуют и для чего они предназначены.
5.1.1 Модульное тестирование (Unit Testing)
Цель: Проверить наименьшие, изолированные части кода (модули, функции,
компоненты) на корректность их работы. Модульный тест должен быть быстрым и
независимым от внешних зависимостей (базы данных, API, файловая система).
Что тестируется: Отдельные функции, методы классов, небольшие компоненты.
Инструменты: Jest, Mocha, Vitest.
Пример: Тестирование функции, которая складывает два числа.
Преимущества:
• Быстрое выполнение.
• Легко локализовать ошибки.
• Помогает в рефакторинге, так как изменения в одном модуле не должны ломать
другие.
Недостатки:
• Не проверяет взаимодействие между модулями.
• Не гарантирует, что вся система работает как единое целое.
5.1.2 Интеграционное тестирование (Integration Testing)
Цель: Проверить взаимодействие между несколькими модулями или компонентами,
убедиться, что они работают вместе корректно. Интеграционные тесты могут
включать взаимодействие с базами данных, файловой системой или внешними API
(часто с использованием моков).
Что тестируется: Группы функций, взаимодействие компонентов, API-маршруты (без
реального запуска сервера).
Инструменты: Jest, Mocha (с дополнительными библиотеками).
Пример: Тестирование API-маршрута, который сохраняет данные в базу данных.
Здесь мы проверяем, что маршрут правильно обрабатывает запрос и что данные
корректно записываются в БД.
Преимущества:
• Выявляет проблемы взаимодействия между компонентами.
• Более полное покрытие функциональности, чем модульные тесты.
Недостатки:
• Медленнее, чем модульные тесты.
• Сложнее локализовать ошибку, так как задействовано несколько компонентов.
5.1.3 Сквозное тестирование (End-to-End Testing, E2E)
Цель: Проверить весь поток приложения от начала до конца, имитируя реальное
взаимодействие пользователя с системой. E2E тесты запускают приложение в
реальном браузере (для фронтенда) или на реальном сервере (для бэкенда) и
проверяют, что все компоненты работают вместе как ожидается.
Что тестируется: Полный пользовательский сценарий, от UI до базы данных и
внешних сервисов.
Инструменты: Playwright, Cypress, Selenium.
Пример: Тестирование сценария регистрации пользователя: пользователь вводит
данные в форму, нажимает кнопку, данные отправляются на сервер, сохраняются в
БД, и пользователь перенаправляется на страницу профиля.
Преимущества:
• Наиболее полное покрытие функциональности.
• Дает высокую уверенность в работоспособности всего приложения.
Недостатки:
• Самые медленные и дорогие в написании и поддержке.
• Могут быть нестабильными (flaky tests) из-за внешних факторов.
• Сложнее отлаживать.
5.2 Jest: Модульное и интеграционное тестирование
Jest — это популярный, простой в использовании и многофункциональный
фреймворк для тестирования JavaScript-кода, разработанный Facebook. Он
поставляется со встроенными функциями для утверждений (assertions), мокирования
(mocking) и покрытия кода (code coverage).
5.2.1 Установка и базовая настройка
Для начала работы с Jest, установите его в ваш проект:
Bash
npm install --save-dev jest
Добавьте скрипт для запуска тестов в [Link] :
JSON
{
"name": "my-app",
"version": "1.0.0",
"description": "",
"main": "[Link]",
"scripts": {
"test": "jest"
},
"keywords": [],
"author": "",
"license": "ISC",
"devDependencies": {
"jest": "^29.7.0"
}
}
Теперь вы можете запускать тесты командой npm test .
5.2.2 Написание модульных тестов
Создайте файл с кодом, который вы хотите протестировать. Например, [Link] :
JavaScript
// [Link]
function sum(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
[Link] = { sum, subtract };
Затем создайте тестовый файл. По соглашению, тестовые файлы называются *.[Link]
или *.[Link] и располагаются рядом с тестируемым кодом или в отдельной папке
__tests__ .
[Link] :
JavaScript
// [Link]
const { sum, subtract } = require("./utils");
describe("Функции утилит", () => {
test("sum(1, 2) должен вернуть 3", () => {
expect(sum(1, 2)).toBe(3);
});
test("subtract(5, 2) должен вернуть 3", () => {
expect(subtract(5, 2)).toBe(3);
});
test("sum(0, 0) должен вернуть 0", () => {
expect(sum(0, 0)).toBe(0);
});
test("sum(-1, 1) должен вернуть 0", () => {
expect(sum(-1, 1)).toBe(0);
});
});
• describe(name, fn) : Группирует связанные тесты. Помогает организовать тестовый
код.
• test(name, fn) или it(name, fn) : Определяет отдельный тестовый случай.
• expect(value) : Начинает утверждение (assertion).
• toBe(expected) : Матчер Jest, который проверяет строгое равенство (как === ).
Jest предоставляет множество других матчеров, таких как toEqual (для сравнения
объектов), toContain (для проверки наличия элемента в массиве), toThrow (для
проверки выброса исключений) и многие другие.
5.2.3 Мокирование (Mocking) и Стабы (Stubbing)
При модульном тестировании часто возникает необходимость изолировать
тестируемый модуль от его зависимостей. Это достигается с помощью моков и стабов.
• Мок (Mock): Имитация объекта, которая записывает, как с ней
взаимодействовали (какие методы были вызваны, с какими аргументами).
Используется для проверки поведения.
• Стаб (Stub): Имитация объекта, которая предоставляет заранее определенные
ответы на вызовы методов. Используется для контроля состояния.
Jest имеет мощные встроенные возможности для мокирования.
Мокирование функций:
JavaScript
// [Link]
const axios = require("axios");
async function fetchUser(id) {
const response = await [Link](`[Link]
return [Link];
}
[Link] = { fetchUser };
JavaScript
// [Link]
const { fetchUser } = require("./api");
const axios = require("axios");
// Мокируем весь модуль axios
[Link]("axios");
describe("fetchUser", () => {
test("должен получить данные пользователя", async () => {
const mockUserData = { id: 1, name: "Alice" };
// Определяем, что должен вернуть [Link] при вызове
[Link]({ data: mockUserData });
const user = await fetchUser(1);
expect(user).toEqual(mockUserData);
// Проверяем, что [Link] был вызван с правильным URL
expect([Link]).toHaveBeenCalledWith("[Link]
});
});
Здесь [Link]("axios") заменяет реальный модуль axios на мок.
[Link]() позволяет нам контролировать, что вернет [Link] при
вызове. toHaveBeenCalledWith() — это матчер Jest для проверки вызовов мок-функций.
Мокирование модулей:
Вы также можете мокировать целые модули, чтобы контролировать их поведение.
JavaScript
// [Link]
const { fetchUser } = require("./api");
async function getUserProfile(userId) {
const user = await fetchUser(userId);
return { ...user, role: "user" };
}
[Link] = { getUserProfile };
JavaScript
// [Link]
const { getUserProfile } = require("./auth");
const api = require("./api");
// Мокируем функцию fetchUser из модуля api
[Link]("./api", () => ({
fetchUser: [Link](),
}));
describe("getUserProfile", () => {
test("должен вернуть профиль пользователя с ролью", async () => {
const mockUserData = { id: 1, name: "Bob" };
[Link](mockUserData);
const profile = await getUserProfile(1);
expect(profile).toEqual({ id: 1, name: "Bob", role: "user" });
expect([Link]).toHaveBeenCalledWith(1);
});
});
5.2.4 Тестирование Express API (интеграционное)
Для тестирования Express API без запуска реального сервера можно использовать
библиотеку supertest .
Установка:
Bash
npm install --save-dev supertest
Пример [Link] (простой Express-сервер):
JavaScript
// [Link]
const express = require("express");
const app = express();
[Link]([Link]());
let items = [{ id: 1, name: "Item 1" }];
[Link]("/api/items", (req, res) => {
[Link](items);
});
[Link]("/api/items", (req, res) => {
const newItem = { id: [Link] + 1, name: [Link] };
[Link](newItem);
[Link](201).json(newItem);
});
[Link] = app; // Экспортируем приложение для тестирования
Пример [Link] :
JavaScript
// [Link]
const request = require("supertest");
const app = require("./app"); // Импортируем наше Express-приложение
describe("API /api/items", () => {
// Сброс состояния перед каждым тестом (если нужно)
beforeEach(() => {
// В реальном приложении здесь может быть очистка БД или сброс моков
// Для этого примера просто сбросим items
[Link] = [{ id: 1, name: "Item 1" }];
});
test("GET /api/items должен вернуть список элементов", async () => {
const response = await request(app).get("/api/items");
expect([Link]).toBe(200);
expect([Link]).toEqual([{ id: 1, name: "Item 1" }]);
});
test("POST /api/items должен создать новый элемент", async () => {
const newItem = { name: "New Item" };
const response = await request(app)
.post("/api/items")
.send(newItem)
.set("Accept", "application/json");
expect([Link]).toBe(201);
expect([Link]).toEqual("New Item");
expect([Link]).toBeDefined();
// Проверяем, что элемент действительно добавлен
const getResponse = await request(app).get("/api/items");
expect([Link]).toBe(2);
});
test("POST /api/items должен вернуть 400, если имя отсутствует", async ()
=> {
const response = await request(app)
.post("/api/items")
.send({}) // Пустое тело
.set("Accept", "application/json");
expect([Link]).toBe(400);
// Добавьте в [Link] валидацию для этого теста
// [Link]("/api/items", (req, res) => { if (![Link]) return
[Link](400).send('Name is required'); ... });
});
});
Этот подход позволяет тестировать маршруты Express, включая middleware и логику
обработки запросов, без необходимости запускать сервер на реальном порту, что
делает тесты быстрыми и надежными.
5.3 Playwright/Cypress: Сквозное тестирование (E2E)
Сквозное тестирование (E2E) имитирует взаимодействие реального пользователя с
вашим приложением, проверяя весь стек технологий от фронтенда до бэкенда и базы
данных. Для JavaScript-приложений популярными инструментами являются
Playwright и Cypress.
5.3.1 Playwright
Playwright — это фреймворк для сквозного тестирования, разработанный Microsoft.
Он позволяет автоматизировать браузеры Chromium, Firefox и WebKit с помощью
единого API. Playwright известен своей скоростью, надежностью и мощными
возможностями для отладки.
Установка:
Bash
npm init playwright
Эта команда интерактивно установит Playwright и создаст базовую конфигурацию.
Пример E2E теста с Playwright:
Предположим, у нас есть простое React-приложение, которое отображает список
задач и позволяет добавлять новые.
tests/[Link] :
JavaScript
// tests/[Link]
const { test, expect } = require("@playwright/test");
[Link]("Приложение Todo", () => {
[Link](async ({ page }) => {
// Переходим на страницу нашего приложения перед каждым тестом
await [Link]("[Link] // Убедитесь, что ваше React-
приложение запущено
});
test("должен отображать заголовок", async ({ page }) => {
await expect([Link]("h1")).toHaveText("Список задач");
});
test("должен добавлять новую задачу", async ({ page }) => {
const todoText = "Купить молоко";
// Вводим текст задачи в поле ввода
await [Link]("input[placeholder='Добавить новую задачу']", todoText);
// Нажимаем кнопку "Добавить"
await [Link]("button:has-text('Добавить')");
// Проверяем, что новая задача появилась в списке
await expect([Link]("li").filter({ hasText: todoText
})).toBeVisible();
// Проверяем, что поле ввода очистилось
await expect([Link]("input[placeholder='Добавить новую
задачу']")).toHaveValue("");
});
test("должен помечать задачу как выполненную", async ({ page }) => {
// Сначала добавим задачу, чтобы было что помечать
await [Link]("input[placeholder='Добавить новую задачу']", "Сделать
домашку");
await [Link]("button:has-text('Добавить')");
// Находим чекбокс рядом с задачей и кликаем по нему
await [Link]("li").filter({ hasText: "Сделать домашку"
}).locator("input[type='checkbox']").check();
// Проверяем, что задача получила соответствующий стиль (например,
зачеркнутый текст)
await expect([Link]("li").filter({ hasText: "Сделать домашку"
})).toHaveClass(/completed/);
});
});
Запуск тестов:
Bash
npx playwright test
Playwright автоматически запускает браузер, выполняет действия и делает
скриншоты в случае ошибок, что очень удобно для отладки.
5.3.2 Cypress
Cypress — это еще один популярный фреймворк для E2E тестирования,
ориентированный на фронтенд-разработчиков. Он работает непосредственно в
браузере, что обеспечивает быстрый и интерактивный опыт разработки тестов.
Установка:
Bash
npm install --save-dev cypress
Открытие Cypress Test Runner:
Bash
npx cypress open
Эта команда откроет графический интерфейс Cypress, где вы сможете выбирать и
запускать тесты, а также видеть их выполнение в реальном времени.
Пример E2E теста с Cypress:
cypress/e2e/[Link] :
JavaScript
// cypress/e2e/[Link]
describe("Приложение Todo", () => {
beforeEach(() => {
[Link]("[Link] // Переходим на страницу нашего
приложения
});
it("должен отображать заголовок", () => {
[Link]("h1").should("[Link]", "Список задач");
});
it("должен добавлять новую задачу", () => {
const todoText = "Купить хлеб";
[Link]("input[placeholder='Добавить новую задачу']").type(todoText);
[Link]("button:contains('Добавить')").click();
[Link]("li").should("contain", todoText);
[Link]("input[placeholder='Добавить новую задачу']").should("[Link]",
"");
});
it("должен помечать задачу как выполненную", () => {
[Link]("input[placeholder='Добавить новую задачу']").type("Позвонить
маме");
[Link]("button:contains('Добавить')").click();
[Link]("li:contains('Позвонить
маме')").find("input[type='checkbox']").check();
[Link]("li:contains('Позвонить маме')").should("[Link]",
"completed");
});
});
Cypress имеет свой собственный API для взаимодействия с элементами страницы
( [Link] , [Link] , [Link] ) и утверждений ( .should ). Он также предоставляет мощные
возможности для отладки, включая снимки DOM и видеозапись выполнения тестов.
5.4 Практические задания по Главе 5
Для закрепления материала и развития практических навыков, выполните
следующие задания.
1. Модульное тестирование функций утилит (Jest):
• Создайте файл [Link] со следующими функциями:
• add(a, b) : возвращает сумму a и b .
• divide(a, b) : возвращает a деленное на b . Должна выбрасывать ошибку,
если b равно 0.
• isEven(num) : возвращает true , если число четное, иначе false .
• Напишите модульные тесты для каждой из этих функций, используя Jest.
Убедитесь, что вы тестируете как успешные сценарии, так и граничные случаи
(например, деление на ноль).
2. Интеграционное тестирование Express API (Jest + Supertest):
• Возьмите ваш API для блога из Главы 4 (с подключенной базой данных).
• Напишите интеграционные тесты для следующих маршрутов, используя Jest и
Supertest:
• GET /api/posts : Проверьте, что возвращается массив постов и статус 200.
• POST /api/posts : Проверьте создание нового поста. Убедитесь, что пост
сохраняется в БД и возвращается статус 201. (Вам нужно будет мокировать
или очищать БД перед каждым тестом).
• GET /api/posts/:id : Проверьте получение конкретного поста. Проверьте
сценарий, когда пост не найден (статус 404).
• POST /api/login : Проверьте успешный вход и получение JWT. Проверьте
неверные учетные данные (статус 401).
• GET /api/protected-data (или любой защищенный маршрут): Проверьте доступ с
валидным токеном и отказ в доступе без токена или с невалидным токеном.
• Подсказка: Для тестов, которые изменяют базу данных, рассмотрите
возможность использования beforeAll / afterAll для подключения/отключения к
тестовой БД и beforeEach / afterEach для очистки данных перед/после каждого
теста.
3. Сквозное тестирование React-приложения (Playwright или Cypress):
• Если у вас есть простое React-приложение (например, Todo-лист или форма
регистрации/входа), напишите для него E2E тесты.
• Сценарии для тестирования:
• Загрузка страницы и отображение основных элементов.
• Взаимодействие с формой (ввод данных, отправка).
• Проверка успешного сообщения или перенаправления после отправки
формы.
• (Для Todo-листа) Добавление, пометка как выполненной и удаление задачи.
• (Для формы входа) Успешный вход и отображение защищенного контента.
• Убедитесь, что ваше React-приложение и Express API запущены перед запуском
E2E тестов.
4. Покрытие кода (Code Coverage):
• Настройте Jest для генерации отчета о покрытии кода. Запустите тесты с
опцией --coverage ( npm test -- --coverage ).
• Проанализируйте отчет. Постарайтесь увеличить покрытие кода для ваших
утилит и API-маршрутов. Помните, что 100% покрытие не всегда означает 100%
отсутствие ошибок, но это хороший показатель качества тестов.
Глава 6: Развертывание и основы DevOps
После того как ваше Full-Stack JavaScript приложение разработано и протестировано,
следующим логичным шагом является его развертывание (deployment) — процесс
размещения приложения на сервере, чтобы оно стало доступным для пользователей
в интернете. Эта глава посвящена основам развертывания и концепциям DevOps,
которые помогают автоматизировать и оптимизировать процесс доставки
программного обеспечения.
Мы рассмотрим различные стратегии развертывания для фронтенда (статический
хостинг, CDN) и бэкенда (серверы, контейнеризация). Особое внимание будет уделено
использованию Docker для контейнеризации приложений, что обеспечивает
согласованность среды между разработкой, тестированием и продакшеном. Мы
также коснемся основ CI/CD (Continuous Integration/Continuous Delivery) — практик,
которые автоматизируют сборку, тестирование и развертывание кода, ускоряя
процесс доставки новых функций и исправлений.
Понимание этих концепций позволит вам не только создавать приложения, но и
эффективно управлять их жизненным циклом, обеспечивая надежную и быструю
доставку ценности пользователям.
6.1 Развертывание фронтенда (статический хостинг, CDN)
Фронтенд-приложения, особенно те, что построены на React, Angular или Vue, после
сборки представляют собой набор статических файлов (HTML, CSS, JavaScript,
изображения). Развертывание таких приложений относительно просто и может быть
выполнено с использованием статического хостинга или CDN.
6.1.1 Статический хостинг
Статический хостинг — это самый простой способ размещения веб-сайтов,
состоящих из статических файлов. Сервер просто отдает файлы по запросу, без
какой-либо серверной логики для генерации страниц.
Преимущества:
• Простота: Легко настроить и управлять.
• Скорость: Файлы отдаются напрямую, без обработки на сервере.
• Безопасность: Меньше уязвимостей, так как нет серверной логики.
• Стоимость: Часто очень дешево или даже бесплатно.
Популярные сервисы статического хостинга:
• Netlify: Очень популярен благодаря простоте развертывания, автоматическому
CI/CD из Git-репозиториев, бесплатным SSL-сертификатам и поддержке функций
без сервера (serverless functions).
• Vercel: Похож на Netlify, ориентирован на [Link], но поддерживает любые
статические сайты и Serverless Functions.
• GitHub Pages: Бесплатный хостинг для статических сайтов прямо из вашего
GitHub-репозитория.
• Firebase Hosting: От Google, предлагает быстрый и безопасный хостинг для веб-
приложений.
• Amazon S3 + CloudFront: Более продвинутое решение для статического хостинга с
использованием хранилища объектов S3 и CDN CloudFront.
Процесс развертывания (пример с Netlify):
1. Сборка проекта: Перед развертыванием необходимо собрать ваше React-
приложение. Обычно это делается командой npm run build или yarn build . Это
создаст папку build (или dist ), содержащую оптимизированные статические
файлы.
2. Подключение к Git: Загрузите ваш код на GitHub, GitLab или Bitbucket.
3. Подключение к Netlify: Войдите в Netlify, выберите "New site from Git",
подключите ваш репозиторий.
4. Настройка сборки: Netlify автоматически определит команду сборки ( npm run
build ) и папку для развертывания ( build ). Вы можете настроить эти параметры
при необходимости.
5. Развертывание: Netlify автоматически соберет и развернет ваше приложение
при каждом пуше в выбранную ветку (например, main ).
6.1.2 CDN (Content Delivery Network)
CDN (Content Delivery Network) — это географически распределенная сеть серверов,
которые доставляют веб-контент пользователям на основе их географического
положения. Цель CDN — ускорить доставку контента, уменьшить задержки и снизить
нагрузку на основной сервер.
Как работает CDN:
Когда пользователь запрашивает контент (например, изображение, CSS-файл,
JavaScript-файл), запрос направляется на ближайший к пользователю сервер CDN
(edge server). Если контент есть на этом сервере, он немедленно отдается
пользователю. Если нет, edge server запрашивает его у основного сервера (origin
server), кэширует его и затем отдает пользователю. Последующие запросы от других
пользователей в этом регионе будут обслуживаться из кэша edge server.
Преимущества:
• Ускорение загрузки: Контент доставляется с ближайшего сервера, что уменьшает
задержки.
• Снижение нагрузки: Основной сервер меньше нагружается, так как большинство
запросов обслуживается CDN.
• Повышенная доступность: Если один edge server выходит из строя, запросы
перенаправляются на другой.
• Улучшенная безопасность: Многие CDN предлагают защиту от DDoS-атак и
другие функции безопасности.
Популярные CDN-провайдеры:
• Cloudflare: Один из самых популярных CDN, предлагает широкий спектр услуг,
включая бесплатный план.
• Amazon CloudFront: CDN от AWS, интегрируется с другими сервисами AWS.
• Akamai: Один из старейших и крупнейших CDN-провайдеров.
Использование CDN:
Многие сервисы статического хостинга (Netlify, Vercel, Firebase Hosting) уже
используют CDN под капотом. Если вы размещаете статические файлы на S3, вы
можете настроить CloudFront для работы с ним. Для JavaScript-библиотек часто
используются публичные CDN, такие как [Link] или [Link] .
6.2 Развертывание бэкенда (серверы, контейнеризация)
Развертывание бэкенд-приложений сложнее, чем фронтенда, так как они требуют
выполнения серверной логики, работы с базами данных и обработки запросов.
Существует несколько подходов к развертыванию бэкенда.
6.2.1 Традиционные серверы (VPS, Dedicated Servers)
VPS (Virtual Private Server) и Dedicated Servers — это классические варианты
хостинга. Вы арендуете виртуальную или физическую машину, на которой
устанавливаете операционную систему, [Link], базу данных и ваше приложение.
Процесс развертывания (общий):
1. Выбор провайдера: DigitalOcean, Linode, Vultr, AWS EC2, Google Cloud Compute
Engine, Azure Virtual Machines.
2. Настройка сервера:
• Подключение по SSH.
• Обновление системы.
• Установка [Link], npm/yarn.
• Установка выбранной базы данных (PostgreSQL, MongoDB и т.д.).
• Установка веб-сервера (Nginx или Apache) для проксирования запросов к
вашему [Link] приложению и обслуживания статических файлов.
3. Развертывание кода:
• Клонирование вашего Git-репозитория на сервер.
• Установка зависимостей ( npm install ).
• Сборка фронтенда (если он обслуживается тем же сервером).
• Настройка переменных окружения ( .env файл).
4. Запуск приложения: Использование менеджера процессов, такого как PM2, для
запуска и поддержания работы вашего [Link] приложения.
• PM2: Популярный менеджер процессов для [Link]. Он позволяет запускать
приложения в фоновом режиме, автоматически перезапускать их при сбоях,
управлять логами и масштабировать приложения на несколько ядер CPU.
5. Настройка Nginx (пример):
Nginx часто используется как обратный прокси-сервер перед [Link]
приложением. Он может обрабатывать SSL-сертификаты, кэшировать
статический контент и распределять нагрузку.
Недостатки традиционного подхода:
• Управление зависимостями: Сложно обеспечить одинаковую среду на разных
серверах.
• Масштабирование: Требует ручной настройки новых серверов.
• Изоляция: Приложения могут конфликтовать из-за общих зависимостей.
6.2.2 Контейнеризация с Docker
Docker — это платформа для разработки, доставки и запуска приложений с
использованием контейнеров. Контейнер — это легковесный, автономный,
исполняемый пакет программного обеспечения, который включает в себя все
необходимое для запуска приложения: код, среду выполнения, системные
инструменты, библиотеки и настройки. Контейнеры обеспечивают согласованность
среды, что решает проблему "у меня на машине работает".
Основные концепции Docker:
• Образ (Image): Шаблон только для чтения, который содержит инструкции для
создания контейнера. Образы создаются из Dockerfile .
• Контейнер (Container): Запускаемый экземпляр образа. Контейнер — это
изолированная среда, в которой работает ваше приложение.
• Dockerfile: Текстовый файл, содержащий инструкции для сборки Docker-образа.
• Docker Compose: Инструмент для определения и запуска многоконтейнерных
Docker-приложений. Позволяет описывать всю архитектуру приложения (бэкенд,
фронтенд, база данных) в одном файле.
Пример Dockerfile для [Link] приложения:
Plain Text
# Используем официальный образ [Link] в качестве базового
FROM node:18-alpine
# Устанавливаем рабочую директорию внутри контейнера
WORKDIR /app
# Копируем [Link] и [Link] для установки зависимостей
COPY package*.json ./
# Устанавливаем зависимости
RUN npm install
# Копируем весь остальной код приложения
COPY . .
# Собираем фронтенд (если он находится в том же репозитории и обслуживается
бэкендом)
# RUN npm run build
# Открываем порт, на котором будет работать приложение
EXPOSE 3000
# Команда для запуска приложения при старте контейнера
CMD ["npm", "start"]
Сборка образа и запуск контейнера:
Bash
docker build -t my-blog-api .
docker run -p 3000:3000 my-blog-api
Пример [Link] для Full-Stack приложения ([Link] + React + MongoDB):
YAML
version: '3.8'
services:
backend:
build: ./backend # Путь к папке с Dockerfile для бэкенда
ports:
- "5000:3000" # Маппинг портов: 5000 на хосте -> 3000 в контейнере
environment:
MONGO_URI: mongodb://mongodb:27017/blogdb # Имя сервиса MongoDB как
хост
JWT_SECRET: super_secret_key
depends_on:
- mongodb
volumes:
- ./backend:/app # Монтируем локальную папку для горячей перезагрузки в
разработке
- /app/node_modules # Исключаем node_modules из монтирования
frontend:
build: ./frontend # Путь к папке с Dockerfile для фронтенда (React)
ports:
- "3000:3000" # Порт React dev server
environment:
REACT_APP_API_URL: [Link] # URL бэкенда
depends_on:
- backend
volumes:
- ./frontend:/app
- /app/node_modules
mongodb:
image: mongo:latest # Используем официальный образ MongoDB
ports:
- "27017:27017"
volumes:
- mongodb_data:/data/db # Сохраняем данные в именованный том
volumes:
mongodb_data:
Запуск всего приложения с Docker Compose:
Bash
docker-compose up --build
Docker и Docker Compose значительно упрощают развертывание, обеспечивая
переносимость и воспроизводимость среды.
6.3 Основы CI/CD (Continuous Integration/Continuous Delivery)
CI/CD (Continuous Integration/Continuous Delivery) — это набор практик, которые
помогают автоматизировать этапы разработки программного обеспечения, от
написания кода до его развертывания в продакшене. Цель CI/CD — ускорить и
обезопасить процесс доставки новых функций и исправлений.
6.3.1 Непрерывная интеграция (Continuous Integration, CI)
Непрерывная интеграция (CI) — это практика, при которой разработчики часто
интегрируют свой код в общую кодовую базу (обычно в центральный репозиторий,
такой как Git). Каждая интеграция автоматически проверяется с помощью
автоматизированной сборки и тестов.
Основные шаги CI:
1. Разработчик коммитит код: Разработчик вносит изменения в код и отправляет
их в Git-репозиторий.
2. Запуск CI-пайплайна: Система CI (например, GitHub Actions, GitLab CI, Jenkins)
обнаруживает новые изменения и запускает пайплайн.
3. Сборка проекта: Код компилируется (если это необходимо) и собирается в
исполняемый артефакт.
4. Запуск тестов: Автоматически запускаются модульные, интеграционные и,
возможно, некоторые E2E тесты.
5. Отчет о статусе: Система CI уведомляет разработчика о результатах сборки и
тестов. Если тесты провалились, разработчик немедленно получает обратную
связь и может исправить проблему.
Преимущества CI:
• Раннее обнаружение ошибок.
• Уменьшение конфликтов при слиянии кода.
• Повышение качества кода.
• Ускорение обратной связи для разработчиков.
6.3.2 Непрерывная доставка (Continuous Delivery, CD)
Непрерывная доставка (CD) — это расширение CI, при котором все изменения кода,
прошедшие CI-пайплайн, автоматически подготавливаются к развертыванию в
продакшене. Это означает, что в любой момент времени вы можете безопасно
развернуть последнюю версию приложения.
Основные шаги CD:
1. Все шаги CI.
2. Развертывание в стейджинг: Приложение автоматически развертывается в
тестовую (staging) среду, которая максимально приближена к продакшену.
3. Дополнительное тестирование: На стейджинге могут быть запущены более
длительные E2E тесты, нагрузочные тесты, ручное тестирование или UAT (User
Acceptance Testing).
4. Готовность к развертыванию: Если все тесты пройдены, приложение считается
готовым к развертыванию в продакшен. Развертывание в продакшен обычно
запускается вручную (по нажатию кнопки).
6.3.3 Непрерывное развертывание (Continuous Deployment)
Непрерывное развертывание — это дальнейшее расширение CD, при котором
каждое изменение кода, прошедшее все автоматизированные тесты, автоматически
развертывается в продакшен без ручного вмешательства. Это высший уровень
автоматизации.
Преимущества CD/Continuous Deployment:
• Быстрая доставка: Новые функции и исправления быстро доходят до
пользователей.
• Снижение рисков: Маленькие, частые изменения менее рискованны, чем
большие, редкие релизы.
• Автоматизация: Уменьшение ручных операций и человеческих ошибок.
Инструменты CI/CD:
• GitHub Actions: Встроенный CI/CD сервис в GitHub, очень популярен.
• GitLab CI/CD: Встроенный CI/CD сервис в GitLab.
• Jenkins: Гибкий, расширяемый сервер автоматизации с открытым исходным
кодом.
• CircleCI, Travis CI, Bitbucket Pipelines: Другие популярные облачные CI/CD
сервисы.
Пример GitHub Actions workflow ( .github/workflows/[Link] ):
YAML
name: CI/CD Pipeline
on:
push:
branches:
- main
pull_request:
branches:
- main
jobs:
build-and-test:
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Set up [Link]
uses: actions/setup-node@v3
with:
node-version: '18'
- name: Install dependencies
run: npm install
- name: Run tests
run: npm test
- name: Build frontend (if applicable)
run: npm run build
# if: success() # Запускать только если предыдущие шаги успешны
# - name: Deploy to Netlify (пример для фронтенда)
# if: [Link] == 'refs/heads/main' && success()
# uses: nwtgck/
[email protected] # with:
# publish-dir: './build'
# production-branch: main
# github-token: ${{ secrets.GITHUB_TOKEN }}
# deploy-message: "Deploy from GitHub Actions"
# env:
# NETLIFY_AUTH_TOKEN: ${{ secrets.NETLIFY_AUTH_TOKEN }}
# NETLIFY_SITE_ID: ${{ secrets.NETLIFY_SITE_ID }}
# - name: Deploy to Heroku (пример для бэкенда)
# if: [Link] == 'refs/heads/main' && success()
# uses: akhileshns/
[email protected] # with:
# heroku_api_key: ${{secrets.HEROKU_API_KEY}}
# heroku_app_name: "your-heroku-app-name"
# heroku_email: "your-heroku-email@[Link]"
Этот YAML-файл описывает простой CI-пайплайн, который запускается при каждом
пуше или пулл-реквесте в ветку main . Он устанавливает [Link], зависимости,
запускает тесты и собирает фронтенд. Закомментированные секции показывают, как
можно добавить шаги развертывания.
6.4 Промежуточный проект: Развертывание Full-Stack приложения
Теперь, когда мы изучили основы развертывания и CI/CD, давайте развернем наше
Full-Stack приложение (React фронтенд + Express бэкенд с БД).
Цель: Развернуть React фронтенд на Netlify/Vercel и Express бэкенд на
DigitalOcean/Heroku/Render, используя Docker и/или PM2.
Шаги (пример с Netlify для фронтенда и Heroku для бэкенда):
Часть 1: Развертывание фронтенда (React) на Netlify
1. Подготовьте React-приложение:
• Убедитесь, что ваше React-приложение готово к сборке ( npm run build ).
• В файле .env вашего React-приложения укажите URL вашего бэкенда.
Например:
2. Создайте репозиторий на GitHub/GitLab: Загрузите код вашего React-
приложения.
3. Разверните на Netlify:
• Зарегистрируйтесь на Netlify (если еще нет).
• Нажмите "Add new site" -> "Import an existing project" -> "Deploy with GitHub" (или
другой Git-провайдер).
• Выберите ваш репозиторий с React-приложением.
• Настройте параметры сборки:
• Build command: npm run build
• Publish directory: build (или dist , в зависимости от вашего проекта)
• Нажмите "Deploy site". Netlify автоматически соберет и развернет ваше
приложение. Вы получите URL для вашего фронтенда.
Часть 2: Развертывание бэкенда (Express) на Heroku (или другом PaaS)
Heroku — это платформа как услуга (PaaS), которая упрощает развертывание [Link]
приложений.
1. Подготовьте Express-приложение:
• Убедитесь, что ваше Express-приложение слушает порт, указанный в
переменной окружения PORT (или 3000 по умолчанию):
• Убедитесь, что все необходимые переменные окружения (например,
MONGO_URI , JWT_SECRET ) настроены для продакшена. Heroku позволяет
добавлять их через панель управления или CLI.
• В [Link] должен быть скрипт start :
2. Создайте репозиторий на GitHub/GitLab: Загрузите код вашего Express-
приложения.
3. Разверните на Heroku:
• Зарегистрируйтесь на Heroku (если еще нет).
• Установите Heroku CLI: [Link]
• Войдите в Heroku через CLI: heroku login
• Создайте новое приложение Heroku:
• Подключите ваш Git-репозиторий к Heroku:
• Добавьте переменные окружения для вашей базы данных и JWT-ключа:
• Разверните код:
• После успешного развертывания вы получите URL вашего бэкенда (например,
[Link] ).
Часть 3: Обновление фронтенда
1. После получения URL бэкенда, обновите REACT_APP_API_URL в .env вашего React-
приложения и повторно разверните фронтенд на Netlify (просто сделайте git
push в main , и Netlify автоматически обновит сайт).
Тестирование:
Откройте ваш развернутый фронтенд в браузере и убедитесь, что он корректно
взаимодействует с развернутым бэкендом. Проверьте все функции, которые вы
реализовали (регистрация, вход, создание постов, комментариев и т.д.).
6.5 Практические задания по Главе 6
Для закрепления материала и развития практических навыков, выполните
следующие задания.
1. Развернуть статическое React-приложение на GitHub Pages/Netlify/Vercel:
• Возьмите любое ваше простое React-приложение (например, Todo-лист из
Главы 2 или 5).
• Соберите его для продакшена ( npm run build ).
• Разверните его на одном из сервисов статического хостинга (GitHub Pages,
Netlify, Vercel). Следуйте их документации.
• Убедитесь, что приложение доступно по публичному URL.
2. Контейнеризация Express API с Docker:
• Возьмите ваш Express API для блога из Главы 4 (с подключенной БД).
• Создайте Dockerfile для вашего Express-приложения, следуя примерам из
главы.
• Соберите Docker-образ ( docker build ).
• Запустите контейнер ( docker run ) и убедитесь, что API доступен по указанному
порту (например, [Link] ).
• Бонус: Если вы используете MongoDB, создайте [Link] для запуска
вашего Express API и MongoDB вместе в отдельных контейнерах.
3. Развертывание Express API на PaaS (Heroku/Render/DigitalOcean App Platform):
• Разверните ваш контейнеризированный (или обычный) Express API на одной из
PaaS-платформ (Heroku, Render, DigitalOcean App Platform). Эти платформы
упрощают процесс развертывания, абстрагируясь от управления серверами.
• Настройте необходимые переменные окружения (например, MONGO_URI ,
JWT_SECRET ) на выбранной платформе.
• Убедитесь, что API доступен по публичному URL и корректно взаимодействует с
вашей базой данных.
4. Настройка простого CI/CD пайплайна с GitHub Actions:
• Создайте новый репозиторий на GitHub для вашего Full-Stack приложения (или
используйте существующий).
• Добавьте файл .github/workflows/[Link] в ваш репозиторий.
• Настройте workflow, который будет:
• При пуше в main ветку: чекаутить код, устанавливать [Link],
устанавливать зависимости, запускать тесты ( npm test ), собирать фронтенд
( npm run build ).
• (Опционально) Добавьте шаг для развертывания фронтенда на Netlify
(используя nwtgck/actions-netlify action) или бэкенда на Heroku (используя
akhileshns/heroku-deploy action), используя секреты GitHub для токенов API.
• Сделайте пуш в main и убедитесь, что пайплайн успешно выполняется.
Глава 7: Безопасность Full-Stack приложений
Безопасность является одним из наиболее критически важных аспектов при
разработке любого веб-приложения. Угрозы постоянно развиваются, и недостаточно
защищенное приложение может привести к утечке данных, финансовым потерям,
потере репутации и юридическим проблемам. В этой главе мы сосредоточимся на
распространенных уязвимостях в Full-Stack JavaScript приложениях и методах их
предотвращения, охватывая как фронтенд, так и бэкенд.
Мы рассмотрим такие атаки, как XSS (Cross-Site Scripting), CSRF (Cross-Site Request
Forgery), SQL/NoSQL инъекции, а также уязвимости, связанные с аутентификацией и
авторизацией. Мы изучим лучшие практики по защите данных, включая хеширование
паролей, безопасное хранение секретов и валидацию входных данных. Также мы
коснемся вопросов безопасности API, таких как Rate Limiting и CORS. Понимание этих
угроз и умение применять соответствующие меры защиты — это фундаментальный
навык для любого разработчика, стремящегося создавать надежные и безопасные
приложения.
7.1 Распространенные уязвимости и их предотвращение
Веб-приложения подвержены множеству атак. Знание наиболее распространенных
уязвимостей и методов их предотвращения является ключом к созданию безопасного
приложения.
7.1.1 Cross-Site Scripting (XSS)
XSS — это тип атаки, при которой злоумышленник внедряет вредоносный клиентский
скрипт (обычно JavaScript) в веб-страницу, просматриваемую другими
пользователями. Когда жертва открывает страницу, вредоносный скрипт
выполняется в ее браузере, получая доступ к кукам, токенам сессии и другим
конфиденциальным данным.
Типы XSS:
• Отраженный XSS (Reflected XSS): Вредоносный скрипт отражается от веб-
сервера в ответе на запрос пользователя (например, через URL-параметр).
• Хранимый XSS (Stored XSS): Вредоносный скрипт сохраняется на сервере
(например, в базе данных) и затем отображается на страницах для других
пользователей (например, в комментариях, сообщениях).
• DOM-based XSS: Уязвимость возникает на стороне клиента, когда JavaScript код
манипулирует DOM, используя данные из URL или других источников без должной
очистки.
Пример уязвимого кода (React):
JavaScript
// ОПАСНО! Уязвимо для XSS
function DisplayComment({ comment }) {
return (
<div dangerouslySetInnerHTML={{ __html: [Link] }} />
);
}
Если [Link] содержит <script>alert('XSS!');</script> , то этот скрипт будет
выполнен.
Предотвращение XSS:
1. Экранирование (Escaping) / Санитизация (Sanitization) входных данных:
• На бэкенде: Всегда очищайте и валидируйте пользовательский ввод перед
сохранением в базу данных или отображением. Используйте библиотеки для
санитизации HTML (например, DOMPurify на бэкенде, если вы работаете с
HTML).
• На фронтенде: Никогда не вставляйте пользовательский ввод напрямую в DOM
без экранирования. React по умолчанию экранирует содержимое, вставляемое
в JSX, что предотвращает большинство XSS. Избегайте использования
dangerouslySetInnerHTML .
2. Content Security Policy (CSP): HTTP-заголовок, который позволяет веб-
разработчикам контролировать ресурсы, которые может загружать браузер на
данной странице. Это может предотвратить выполнение вредоносных скриптов,
даже если они были внедрены.
• Пример заголовка CSP:
Content-Security-Policy: default-src 'self'; script-src 'self' [Link]
7.1.2 Cross-Site Request Forgery (CSRF)
CSRF — это атака, при которой злоумышленник заставляет аутентифицированного
пользователя выполнить нежелательное действие на веб-сайте, на котором
пользователь уже вошел в систему. Атака использует доверие сайта к браузеру
пользователя.
Пример: Пользователь вошел в свой банк. Злоумышленник отправляет ему
фишинговое письмо со ссылкой на вредоносный сайт. На этом сайте есть скрытая
форма, которая автоматически отправляет запрос на перевод денег в банк
пользователя. Если пользователь кликнет по ссылке, его браузер отправит запрос с
его куками, и банк выполнит перевод.
Предотвращение CSRF:
1. CSRF-токены: Самый распространенный и эффективный метод. Сервер
генерирует уникальный, случайный токен для каждой сессии пользователя и
встраивает его в формы или JavaScript. При каждом запросе, изменяющем
состояние (POST, PUT, DELETE), клиент должен отправить этот токен обратно на
сервер. Сервер проверяет соответствие токена. Если токены не совпадают,
запрос отклоняется.
• Для Express можно использовать библиотеку csurf .
2. SameSite Cookies: Атрибут SameSite для куки позволяет браузеру
контролировать, когда куки отправляются с межсайтовыми запросами. Установка
SameSite=Lax или SameSite=Strict может значительно снизить риск CSRF.
3. Проверка заголовка Origin или Referer : На бэкенде можно проверять эти
заголовки, чтобы убедиться, что запрос пришел с ожидаемого домена. Однако это
не всегда надежно.
7.1.3 SQL/NoSQL инъекции
Инъекции — это атаки, при которых злоумышленник внедряет вредоносный код
(SQL-запросы, NoSQL-операторы или команды ОС) во входные данные приложения,
чтобы манипулировать логикой базы данных или системы.
Пример уязвимого кода (SQL):
JavaScript
// ОПАСНО! Уязвимо для SQL-инъекций
[Link]("/users/:id", (req, res) => {
const userId = [Link];
// Если userId =
`1 OR 1=1`
`const query = `SELECT * FROM users WHERE id = ${userId}`;
[Link](query, (err, result) => { /* ... */ });
});
Если userId будет 1 OR 1=1 , то запрос станет SELECT * FROM users WHERE id = 1 OR 1=1 , что
вернет всех пользователей.
Предотвращение инъекций:
1. Параметризованные запросы (Prepared Statements): Самый эффективный
способ. Вместо того чтобы вставлять пользовательский ввод напрямую в SQL-
запрос, вы используете заполнители (placeholders), а затем передаете значения
отдельно. База данных сама позаботится об экранировании.
• Для SQL (например, pg для PostgreSQL):
• Для NoSQL (например, Mongoose для MongoDB): Mongoose по умолчанию
использует параметризованные запросы и предотвращает NoSQL-инъекции,
если вы используете его API для построения запросов.
2. Валидация и очистка ввода: Всегда валидируйте и очищайте пользовательский
ввод на стороне сервера. Например, если ожидается число, убедитесь, что это
действительно число. Если ожидается строка, удалите или экранируйте
специальные символы.
7.1.4 Уязвимости аутентификации и авторизации
Аутентификация — это проверка личности пользователя. Авторизация — это
определение прав доступа пользователя к ресурсам.
Распространенные уязвимости:
• Слабые пароли: Использование простых, легко угадываемых паролей.
• Отсутствие хеширования паролей: Хранение паролей в открытом виде или с
использованием слабых алгоритмов хеширования.
• Отсутствие Rate Limiting: Позволяет злоумышленникам проводить атаки
методом перебора (brute-force) на страницы входа.
• Неправильная обработка сессий/токенов: Утечка токенов, отсутствие срока
действия, отсутствие отзыва токенов.
• Недостаточная авторизация (Broken Access Control): Пользователь может
получить доступ к ресурсам, к которым у него не должно быть прав (например,
административные функции или данные других пользователей).
Предотвращение:
1. Надежное хеширование паролей: Всегда используйте сильные, односторонние
алгоритмы хеширования с солью (salt), такие как bcrypt или scrypt . Никогда не
храните пароли в открытом виде.
2. Rate Limiting: Ограничивайте количество запросов к маршрутам аутентификации
(и другим чувствительным маршрутам) с одного IP-адреса за определенный
период времени. Это предотвращает атаки перебора.
• Для Express можно использовать express-rate-limit .
3. Безопасное управление сессиями/токенами:
• Используйте короткие сроки действия для JWT.
• Храните JWT в HttpOnly куках для защиты от XSS (для фронтенда).
• Реализуйте механизм отзыва токенов (например, черный список на сервере).
4. Строгая авторизация: Всегда проверяйте права пользователя на доступ к
ресурсу на стороне сервера. Не доверяйте данным, приходящим с клиента.
Используйте middleware для проверки ролей и разрешений.
7.2 Защита данных и секретов
7.2.1 Хеширование паролей
Как уже упоминалось, хеширование паролей является фундаментальным аспектом
безопасности. Никогда не храните пароли в открытом виде. Используйте
криптографически стойкие, односторонние хеш-функции, которые включают "соль"
(salt) для предотвращения атак по радужным таблицам.
bcrypt — это рекомендуемая библиотека для хеширования паролей в [Link]. Она
медленная по своей природе, что затрудняет атаки перебора.
JavaScript
const bcrypt = require("bcryptjs");
// Регистрация пользователя
async function registerUser(username, password) {
const salt = await [Link](10); // Генерируем соль (10 - это
количество раундов)
const hashedPassword = await [Link](password, salt); // Хешируем
пароль с солью
// Сохраняем hashedPassword в базу данных
return { username, hashedPassword };
}
// Вход пользователя
async function loginUser(username, password, storedHashedPassword) {
const isMatch = await [Link](password, storedHashedPassword); //
Сравниваем введенный пароль с хешированным
return isMatch; // true, если пароли совпадают
}
// Пример использования:
// (async () => {
// const user = await registerUser("testuser", "MySuperSecretPassword123");
// [Link]("Хешированный пароль:", [Link]);
// const loggedIn = await loginUser("testuser", "MySuperSecretPassword123",
[Link]);
// [Link]("Вход успешен:", loggedIn);
// })();
7.2.2 Безопасное хранение секретов (переменные окружения)
Конфиденциальные данные, такие как ключи API, секретные ключи JWT, учетные
данные базы данных, должны храниться безопасно и никогда не должны быть жестко
закодированы в коде или храниться в системах контроля версий (Git).
Переменные окружения — это стандартный способ хранения секретов. В [Link] вы
можете использовать библиотеку dotenv для загрузки переменных окружения из
файла .env в процессе разработки.
Файл .env (не добавляйте его в Git!):
Plain Text
PORT=3000
MONGO_URI=mongodb+srv://user:password@[Link]/mydb
JWT_SECRET=super_secret_jwt_key_that_is_long_and_random
API_KEY_STRIPE=sk_test_xxxxxxxxxxxx
Использование в коде:
JavaScript
require("dotenv").config(); // Загружает переменные из .env
const port = [Link];
const mongoUri = [Link].MONGO_URI;
const jwtSecret = [Link].JWT_SECRET;
[Link]("Порт:", port);
[Link]("JWT Secret (не выводите в продакшене!):"); // Никогда не
логируйте секреты в продакшене
В продакшене: Переменные окружения устанавливаются непосредственно на
сервере или в PaaS-платформе (Heroku, Render, AWS Elastic Beanstalk и т.д.).
7.2.3 Валидация входных данных
Валидация входных данных — это процесс проверки того, что данные, полученные
от пользователя (или любого внешнего источника), соответствуют ожидаемому
формату, типу и ограничениям. Это критически важно для предотвращения многих
типов атак, включая инъекции, XSS и переполнение буфера.
Лучшие практики:
• Валидация на стороне сервера: Всегда валидируйте данные на бэкенде, даже
если вы уже валидировали их на фронтенде. Фронтенд-валидацию легко обойти.
• Использование библиотек: Используйте специализированные библиотеки для
валидации, такие как Joi , Yup , express-validator .
• "Белый список" (Whitelist) вместо "Черного списка" (Blacklist): Разрешайте
только известные хорошие данные, а не пытайтесь запретить известные плохие.
Например, для email-адреса проверяйте, что он соответствует формату email, а не
пытайтесь отфильтровать все возможные вредоносные символы.
Пример с express-validator :
Установка:
Bash
npm install express-validator
Использование:
JavaScript
const { body, validationResult } = require("express-validator");
[Link](
"/register",
[
body("username")
.isLength({ min: 3 })
.withMessage("Имя пользователя должно быть не менее 3 символов"),
body("email").isEmail().withMessage("Некорректный формат email"),
body("password")
.isLength({ min: 6 })
.withMessage("Пароль должен быть не менее 6 символов"),
],
(req, res) => {
const errors = validationResult(req);
if (![Link]()) {
return [Link](400).json({ errors: [Link]() });
}
// Если валидация прошла успешно, продолжаем обработку
const { username, email, password } = [Link];
// ... логика регистрации пользователя
[Link](201).json({ message: "Пользователь зарегистрирован" });
}
);
7.3 Безопасность API (CORS, Rate Limiting)
7.3.1 CORS (Cross-Origin Resource Sharing)
CORS — это механизм, который позволяет веб-странице запрашивать ресурсы с
другого домена, чем тот, с которого она была загружена. По умолчанию браузеры
применяют политику Same-Origin Policy, которая запрещает такие запросы из
соображений безопасности. CORS предоставляет способ для сервера явно разрешить
междоменные запросы.
Когда нужен CORS:
Когда ваш фронтенд (например, на [Link] или [Link] )
пытается сделать запрос к вашему бэкенду (например, на [Link] или
[Link] ).
Решение: Сервер должен отправлять соответствующие HTTP-заголовки Access-Control-
Allow-Origin .
Использование cors middleware для Express:
Установка:
Bash
npm install cors
Использование:
JavaScript
const express = require("express");
const cors = require("cors");
const app = express();
// Разрешить все CORS запросы (для разработки)
[Link](cors());
// Или настроить CORS для конкретных источников (для продакшена)
const corsOptions = {
origin: "[Link] // Разрешить запросы только с этого
домена
methods: "GET,HEAD,PUT,PATCH,POST,DELETE",
credentials: true, // Разрешить отправку куки и заголовков авторизации
optionsSuccessStatus: 204,
};
[Link](cors(corsOptions));
// ... ваши маршруты
В продакшене всегда указывайте конкретные домены, которым разрешен доступ, а не
*.
7.3.2 Rate Limiting
Rate Limiting — это техника, которая ограничивает количество запросов, которые
пользователь или IP-адрес может сделать к серверу за определенный период
времени. Это помогает предотвратить атаки типа DoS (Denial of Service), brute-force
атаки и злоупотребление API.
Использование express-rate-limit для Express:
Установка:
Bash
npm install express-rate-limit
Использование:
JavaScript
const express = require("express");
const rateLimit = require("express-rate-limit");
const app = express();
// Применить ограничение ко всем запросам
const apiLimiter = rateLimit({
windowMs: 15 * 60 * 1000, // 15 минут
max: 100, // Максимум 100 запросов за 15 минут с одного IP
message: "Слишком много запросов с этого IP, попробуйте позже.",
});
[Link](apiLimiter);
// Или применить к конкретным маршрутам (например, для логина)
const loginLimiter = rateLimit({
windowMs: 5 * 60 * 1000, // 5 минут
max: 5, // Максимум 5 попыток входа за 5 минут
message: "Слишком много попыток входа, попробуйте через 5 минут.",
standardHeaders: true, // Возвращает заголовки RateLimit-Limit, RateLimit-
Remaining, RateLimit-Reset
legacyHeaders: false, // Отключает заголовки X-RateLimit-Limit, X-
RateLimit-Remaining, X-RateLimit-Reset
});
[Link]("/login", loginLimiter, (req, res) => {
// ... логика входа
});
7.4 Практические задания по Главе 7
Для закрепления материала и развития практических навыков, выполните
следующие задания.
1. Улучшить безопасность API блога:
• Возьмите ваш API для блога из Главы 4 (с подключенной БД).
• Хеширование паролей: Убедитесь, что при регистрации и входе используются
bcryptjs для хеширования и проверки паролей.
• Валидация ввода: Используйте express-validator для валидации входных
данных для маршрутов POST /api/register , POST /api/login , POST /api/posts , POST
/api/posts/:postId/comments . Убедитесь, что данные соответствуют ожидаемым
типам и форматам.
• Rate Limiting: Примените express-rate-limit к маршруту POST /api/login
(например, 5 запросов за 5 минут) и к общим API-маршрутам (например, 100
запросов за 15 минут).
• CORS: Настройте cors middleware так, чтобы он разрешал запросы только с
вашего фронтенд-домена (например, [Link] для разработки, или
ваш Netlify/Vercel домен для продакшена).
• Безопасное хранение секретов: Убедитесь, что JWT_SECRET и учетные данные
БД загружаются из переменных окружения ( .env для разработки).
2. Тестирование уязвимостей (ручное):
• Запустите ваш улучшенный API блога.
• Попробуйте провести ручные тесты на уязвимости:
• XSS: Попробуйте создать пост или комментарий, содержащий
<script>alert("XSS!");</script> . Убедитесь, что скрипт не выполняется в браузере
(если вы используете React, он должен автоматически экранировать). Если
вы выводите данные на бэкенде, убедитесь, что они экранируются.
• SQL/NoSQL инъекции: Попробуйте отправить запросы с вредоносными
строками в параметрах или теле запроса (например, id=1 OR 1=1 ). Убедитесь,
что сервер не возвращает неожиданные данные или ошибки.
• Broken Access Control: Если у вас есть роли (например, admin/user),
попробуйте получить доступ к административным маршрутам, будучи
обычным пользователем. Убедитесь, что сервер возвращает 403 Forbidden.
• Rate Limiting: Попробуйте отправить более 5 запросов на POST /api/login за 5
минут. Убедитесь, что вы получаете ошибку 429 Too Many Requests.
3. Изучить [Link]:
• Исследуйте библиотеку helmet для [Link]. Она помогает защитить ваше
приложение, устанавливая различные HTTP-заголовки, связанные с
безопасностью.
• Установите helmet и примените его ко всему вашему Express-приложению.
• Проанализируйте, какие заголовки добавляет helmet и как они улучшают
безопасность.
Глава 8: Производительность и оптимизация
Производительность является ключевым фактором успеха любого веб-приложения.
Медленно загружающиеся страницы, задержки в ответах сервера и
неоптимизированный код могут отпугнуть пользователей, снизить конверсию и
негативно сказаться на SEO. В этой главе мы сосредоточимся на методах и
инструментах для измерения, анализа и улучшения производительности Full-Stack
JavaScript приложений, охватывая как фронтенд, так и бэкенд.
Мы рассмотрим техники оптимизации загрузки фронтенда (ленивая загрузка, сжатие
изображений, оптимизация CSS/JS), а также методы повышения производительности
бэкенда (кэширование, оптимизация запросов к БД, балансировка нагрузки). Мы
научимся использовать инструменты разработчика браузера для профилирования
фронтенда и инструменты мониторинга для бэкенда. Понимание того, как сделать
ваше приложение быстрым и отзывчивым, является критически важным навыком для
создания высококачественных пользовательских интерфейсов и масштабируемых
серверных решений.
8.1 Оптимизация фронтенда
Производительность фронтенда напрямую влияет на пользовательский опыт.
Быстрая загрузка и отзывчивый интерфейс — залог успеха. Рассмотрим основные
методы оптимизации.
8.1.1 Ленивая загрузка (Lazy Loading)
Ленивая загрузка — это техника, при которой ресурсы (изображения, видео,
компоненты, маршруты) загружаются только тогда, когда они действительно нужны,
а не при первоначальной загрузке страницы. Это значительно сокращает время
первой отрисовки (First Contentful Paint) и время до интерактивности (Time to
Interactive).
Примеры ленивой загрузки:
• Изображения и видео: Атрибут loading="lazy" для тегов <img> и <video> .
• Компоненты React: Используйте [Link]() и Suspense для динамического
импорта компонентов.
• Маршруты (Code Splitting): Разделение кода по маршрутам позволяет загружать
только тот JavaScript, который необходим для текущей страницы.
8.1.2 Оптимизация изображений
Изображения часто являются самым тяжелым контентом на веб-странице.
Правильная оптимизация может значительно ускорить загрузку.
• Правильный формат:
• JPEG: Для фотографий и изображений со сложными цветами.
• PNG: Для изображений с прозрачностью или четкими линиями (логотипы,
иконки).
• WebP: Современный формат, разработанный Google, обеспечивающий лучшее
сжатие при сохранении качества. Поддерживается большинством современных
браузеров.
• SVG: Для векторной графики (иконки, логотипы), масштабируется без потери
качества.
• Сжатие: Используйте инструменты для сжатия изображений без потери качества
(lossless) или с минимальной потерей (lossy). Онлайн-инструменты: TinyPNG,
[Link]. Инструменты командной строки: imagemin .
• Размеры: Используйте изображения, соответствующие размеру, в котором они
будут отображаться. Не загружайте 4K изображение, если оно будет показано в
размере 200x200px.
• Адаптивные изображения: Используйте атрибуты srcset и sizes или тег
<picture> для предоставления разных версий изображений для разных размеров
экрана и разрешений.
8.1.3 Минификация и сжатие CSS/JS
Минификация — это процесс удаления всех ненужных символов из исходного кода
(пробелы, комментарии, переносы строк) без изменения его функциональности.
Сжатие (например, Gzip или Brotli) — это алгоритмы, которые уменьшают размер
файлов для передачи по сети.
• Минификация: Большинство современных сборщиков (Webpack, Rollup, Vite)
автоматически минифицируют код при сборке для продакшена. Для JavaScript
используется Terser , для CSS — cssnano .
• Сжатие на сервере: Убедитесь, что ваш веб-сервер (Nginx, Apache) или CDN
настроены на сжатие статических файлов (CSS, JS, HTML) перед их отправкой
клиенту. Это значительно уменьшает объем передаваемых данных.
• Nginx (пример конфигурации):
8.1.4 Оптимизация шрифтов
Веб-шрифты могут быть тяжелыми. Оптимизируйте их загрузку:
• Используйте font-display: swap; : Позволяет браузеру использовать системный
шрифт, пока пользовательский шрифт загружается, предотвращая "невидимый
текст" (FOIT - Flash of Invisible Text).
• Подмножества шрифтов (Subsetting): Включайте только те символы и
начертания, которые вам действительно нужны.
• Форматы: Используйте современные форматы шрифтов, такие как WOFF2,
которые обеспечивают лучшее сжатие.
8.2 Оптимизация бэкенда
Производительность бэкенда критична для масштабируемости и отзывчивости
приложения. Медленные запросы к базе данных или неэффективная обработка
данных могут стать узким местом.
8.2.1 Кэширование
Кэширование — это процесс хранения копий данных в быстродоступном месте,
чтобы последующие запросы к этим данным могли быть обслужены быстрее, чем при
их повторном получении из исходного источника (например, базы данных).
Уровни кэширования:
• Кэширование на стороне клиента (браузерный кэш): Браузер кэширует
статические ресурсы (CSS, JS, изображения) на основе HTTP-заголовков Cache-
Control и Expires .
• CDN-кэширование: CDN кэширует статический контент ближе к пользователям.
• Кэширование на стороне сервера (Application Cache): Приложение кэширует
результаты дорогостоящих операций (например, результаты запросов к БД,
ответы API) в памяти или в специализированных кэш-хранилищах (Redis,
Memcached).
• Кэширование базы данных: Базы данных имеют свои механизмы кэширования
запросов и данных.
Пример кэширования в Express с Redis:
Установка Redis: Установите Redis локально или используйте облачный сервис.
Установка redis и express-redis-cache (или node-cache для in-memory):
Bash
npm install redis express-redis-cache
JavaScript
const express = require("express");
const redis = require("redis");
const cache = require("express-redis-cache")({ client: [Link]()
});
const app = express();
// Маршрут, который будет кэшироваться на 1 минуту
[Link]("/api/posts", [Link]({ expire: 60 }), async (req, res) => {
// Эта логика будет выполняться только при первом запросе или после
истечения кэша
[Link]("Получение постов из базы данных...");
const posts = await [Link](); // Предполагаем, что Post - это модель
Mongoose
[Link](posts);
});
// Пример ручного кэширования
const NodeCache = require("node-cache");
const myCache = new NodeCache({ stdTTL: 600 }); // Кэш на 10 минут
[Link]("/api/users/:id", async (req, res) => {
const userId = [Link];
let user = [Link](userId); // Пытаемся получить из кэша
if (user) {
[Link]("Пользователь из кэша.");
return [Link](user);
}
[Link]("Получение пользователя из базы данных...");
user = await [Link](userId); // Получаем из БД
if (user) {
[Link](userId, user); // Сохраняем в кэш
}
[Link](user);
});
8.2.2 Оптимизация запросов к БД
Медленные запросы к базе данных — одна из самых частых причин низкой
производительности бэкенда.
• Индексы: Убедитесь, что у вас есть индексы на столбцах, которые часто
используются в условиях WHERE , JOIN , ORDER BY (см. Главу 4).
• Избегайте N+1 проблемы: Это когда вы делаете N дополнительных запросов к
базе данных для получения связанных данных, вместо одного запроса.
Используйте populate в Mongoose или include в Sequelize для "жадной" загрузки
связанных данных.
• Плохо (N+1):
• Хорошо (с populate ):
• Выбирайте только нужные поля: Не выбирайте SELECT * если вам нужны только
несколько полей. Это уменьшает объем передаваемых данных.
• Mongoose: [Link]().select("username email")
• Sequelize: [Link]({ attributes: ["username", "email"] })
• Пагинация: Для больших наборов данных всегда используйте пагинацию
(LIMIT/OFFSET в SQL, skip / limit в MongoDB) для ограничения количества
возвращаемых записей.
• Агрегации: Используйте агрегационные фреймворки (например, MongoDB
Aggregation Pipeline) или сложные SQL-запросы для выполнения вычислений на
стороне базы данных, а не в приложении.
8.2.3 Балансировка нагрузки (Load Balancing)
Балансировка нагрузки — это распределение входящего сетевого трафика между
несколькими серверами. Это помогает повысить доступность, масштабируемость и
надежность приложения, предотвращая перегрузку одного сервера.
• Как работает: Балансировщик нагрузки (Load Balancer) принимает входящие
запросы и перенаправляет их на один из доступных серверов в пуле, используя
различные алгоритмы (например, Round Robin, Least Connections).
• Преимущества:
• Высокая доступность: Если один сервер выходит из строя, трафик
перенаправляется на другие.
• Масштабируемость: Легко добавлять новые серверы для обработки
возросшей нагрузки.
• Улучшенная производительность: Равномерное распределение нагрузки
предотвращает перегрузку отдельных серверов.
• Инструменты: Nginx (может выступать как простой балансировщик), HAProxy,
облачные балансировщики нагрузки (AWS ELB, Google Cloud Load Balancing).
8.2.4 Оптимизация [Link] приложения
• Используйте асинхронные операции: [Link] однопоточен, поэтому все
операции ввода/вывода должны быть асинхронными, чтобы не блокировать
Event Loop.
• Профилирование: Используйте встроенные инструменты [Link] для
профилирования (например, node --inspect ) или сторонние инструменты
(например, [Link] ) для выявления узких мест в коде.
• Кластеризация (Clustering): Используйте встроенный модуль cluster [Link] для
запуска нескольких процессов вашего приложения на одном сервере, чтобы
использовать все ядра CPU.
• Мониторинг: Используйте инструменты мониторинга (Prometheus, Grafana, New
Relic, Datadog) для отслеживания производительности приложения в
продакшене.
8.3 Инструменты для измерения и анализа производительности
8.3.1 Инструменты разработчика браузера (Chrome DevTools)
Chrome DevTools — это мощный набор инструментов, встроенных в браузер Chrome,
которые позволяют анализировать производительность фронтенда.
• Вкладка Performance: Записывает активность страницы во время загрузки и
взаимодействия. Позволяет увидеть, что происходит на каждом этапе: загрузка
ресурсов, парсинг HTML, выполнение JavaScript, расчет стилей, отрисовка.
Помогает выявить узкие места в рендеринге и скриптах.
• Вкладка Network: Показывает все сетевые запросы, сделанные страницей, их
размер, время загрузки, заголовки. Помогает выявить медленные запросы,
большие ресурсы, проблемы с кэшированием.
• Вкладка Lighthouse: Автоматизированный инструмент для аудита качества веб-
страниц. Он генерирует отчеты по производительности, доступности, лучшим
практикам, SEO и PWA. Дает конкретные рекомендации по улучшению.
• Вкладка Memory: Позволяет профилировать использование памяти JavaScript-
приложением, выявлять утечки памяти.
8.3.2 Web Vitals
Core Web Vitals — это набор метрик, разработанных Google, которые измеряют
реальный пользовательский опыт загрузки, интерактивности и визуальной
стабильности веб-страницы. Они являются важным фактором ранжирования в Google
Search.
• Largest Contentful Paint (LCP): Измеряет время загрузки самого большого
элемента контента на странице (изображение, видео, блок текста). Должен быть
менее 2.5 секунд.
• First Input Delay (FID): Измеряет время от первого взаимодействия пользователя
со страницей (клик, нажатие клавиши) до момента, когда браузер смог
отреагировать на это взаимодействие. Должен быть менее 100 мс.
• Cumulative Layout Shift (CLS): Измеряет визуальную стабильность страницы. Это
сумма всех неожиданных сдвигов макета, которые происходят во время загрузки
страницы. Должен быть менее 0.1.
Вы можете измерять Web Vitals с помощью Lighthouse, PageSpeed Insights, Chrome User
Experience Report (CrUX) и других инструментов.
8.3.3 Мониторинг производительности приложений (APM)
APM (Application Performance Monitoring) — это инструменты, которые позволяют
отслеживать и анализировать производительность бэкенд-приложений в реальном
времени. Они собирают метрики о запросах, ошибках, использовании ресурсов,
производительности базы данных и многом другом.
Популярные APM-инструменты:
• New Relic: Комплексное решение для мониторинга всего стека.
• Datadog: Мониторинг, логирование, трассировка, метрики.
• Prometheus + Grafana: Открытые инструменты для сбора метрик и визуализации.
• Sentry: В основном для отслеживания ошибок, но также предоставляет
некоторую информацию о производительности.
Эти инструменты помогают выявлять узкие места в продакшене, отслеживать
деградацию производительности и быстро реагировать на проблемы.
8.4 Практические задания по Главе 8
Для закрепления материала и развития практических навыков, выполните
следующие задания.
1. Оптимизация React-приложения:
• Возьмите ваше React-приложение (например, Todo-лист или любое другое,
которое вы использовали).
• Ленивая загрузка: Реализуйте ленивую загрузку для одного из компонентов
или маршрутов, используя [Link]() и Suspense .
• Оптимизация изображений: Если ваше приложение использует изображения,
убедитесь, что они оптимизированы (правильный формат, сжатие, адаптивные
размеры). Используйте loading="lazy" для изображений.
• Анализ с помощью Lighthouse: Запустите аудит Lighthouse (в Chrome DevTools)
для вашего приложения. Проанализируйте отчет и постарайтесь исправить
хотя бы 2-3 рекомендации по улучшению производительности.
2. Оптимизация Express API:
• Возьмите ваш Express API для блога из Главы 4 (с подключенной БД).
• Кэширование: Реализуйте кэширование для маршрута GET /api/posts (или
любого другого, который часто запрашивается) с использованием express-redis-
cache (если у вас есть Redis) или node-cache (для in-memory кэша).
• Оптимизация запросов к БД:
• Убедитесь, что все запросы к БД используют индексы там, где это
необходимо (например, для поиска по ID, username, email).
• Проверьте, нет ли N+1 проблемы в ваших запросах. Если есть, исправьте ее,
используя populate (Mongoose) или include (Sequelize).
• Убедитесь, что вы выбираете только необходимые поля из БД, а не все
( .select() или attributes ).
• Профилирование: Используйте node --inspect для профилирования вашего
Express-приложения. Запустите его с этой опцией, затем откройте Chrome
DevTools -> More tools -> Profiler и запишите профиль во время выполнения
нескольких запросов к API. Проанализируйте, какие функции занимают больше
всего времени.
3. Настройка Gzip/Brotli сжатия:
• Если вы используете Nginx для обслуживания вашего фронтенда или
проксирования к бэкенду, убедитесь, что Gzip (или Brotli) сжатие включено и
настроено правильно для статических файлов (CSS, JS, HTML) и ответов API.
• Проверьте, что сжатие работает, используя вкладку Network в Chrome DevTools
(посмотрите на заголовок Content-Encoding ).
4. Кластеризация [Link] (бонусное задание):
• Реализуйте кластеризацию для вашего Express API, используя встроенный
модуль cluster [Link]. Запустите столько рабочих процессов, сколько ядер
CPU на вашей машине.
• Проверьте, что все процессы запущены, используя pm2 list (если вы
используете PM2) или системные команды.
Глава 9: Расширенные темы и лучшие практики
По мере того как вы углубляетесь в Full-Stack JavaScript разработку, вы столкнетесь с
более сложными концепциями и лучшими практиками, которые помогут вам
создавать более надежные, масштабируемые и поддерживаемые приложения. Эта
глава охватывает ряд продвинутых тем, которые выходят за рамки базового курса, но
являются важными для становления полноценным специалистом.
Мы рассмотрим микросервисную архитектуру как альтернативу монолиту, принципы
чистого кода и архитектуры, а также важность документации и стандартов
кодирования. Также будут затронуты темы интернационализации (i18n) и
локализации (l10n), что позволит вашим приложениям быть доступными для
глобальной аудитории. Понимание этих концепций поможет вам не только писать
лучший код, но и эффективно работать в команде, создавая сложные и устойчивые
системы.
9.1 Микросервисная архитектура
Микросервисная архитектура — это подход к разработке программного
обеспечения, при котором приложение строится как набор небольших, автономных
сервисов, каждый из которых работает в своем собственном процессе и
взаимодействует с другими сервисами через легковесные механизмы (обычно HTTP
API). Это контрастирует с традиционной монолитной архитектурой, где все
компоненты приложения объединены в единый, неделимый блок.
9.1.1 Монолит против Микросервисов
Монолитная архитектура:
• Преимущества:
• Простота разработки и развертывания на начальных этапах.
• Единая кодовая база, что упрощает отладку и тестирование.
• Меньше накладных расходов на межсервисное взаимодействие.
• Недостатки:
• Сложность: По мере роста приложения монолит становится все более
сложным и трудным для понимания и модификации.
• Масштабирование: Масштабировать можно только все приложение целиком,
даже если нагрузка высока только на один компонент.
• Технологический стек: Трудно использовать разные технологии для разных
частей приложения.
• Развертывание: Небольшое изменение требует пересборки и
переразвертывания всего приложения.
• Отказоустойчивость: Сбой в одном компоненте может привести к падению
всего приложения.
Микросервисная архитектура:
• Преимущества:
• Масштабируемость: Каждый сервис может масштабироваться независимо.
• Гибкость технологий: Разные сервисы могут быть написаны на разных языках
и использовать разные базы данных.
• Устойчивость: Сбой в одном сервисе не обязательно приводит к падению всего
приложения.
• Независимое развертывание: Сервисы могут развертываться независимо
друг от друга.
• Разделение ответственности: Каждый сервис отвечает за свою конкретную
бизнес-функцию.
• Команды: Небольшие команды могут работать над отдельными сервисами.
• Недостатки:
• Сложность: Распределенная система сложнее в разработке, тестировании,
развертывании и мониторинге.
• Межсервисное взаимодействие: Требует надежных механизмов связи (API
Gateway, очереди сообщений).
• Управление данными: Распределенные транзакции и согласованность данных
становятся сложнее.
• Мониторинг и логирование: Требует централизованных систем.
9.1.2 Когда использовать микросервисы?
Микросервисы не являются серебряной пулей и не подходят для всех проектов. Они
наиболее полезны для:
• Больших, сложных приложений: Где монолит становится неуправляемым.
• Команд с высокой степенью автономии: Где каждая команда может владеть
своим сервисом.
• Приложений, требующих независимого масштабирования компонентов.
• Организаций, готовых инвестировать в инфраструктуру и инструменты для
управления распределенными системами.
Для небольших и средних проектов монолит часто является более простым и
эффективным решением.
9.1.3 Взаимодействие между микросервисами
• HTTP/REST API: Самый распространенный способ. Сервисы общаются друг с
другом через HTTP-запросы.
• Очереди сообщений (Message Queues): Для асинхронного взаимодействия.
Сервис отправляет сообщение в очередь, а другой сервис его потребляет.
Примеры: RabbitMQ, Apache Kafka, AWS SQS.
• gRPC: Высокопроизводительный фреймворк для удаленного вызова процедур,
использующий HTTP/2 и Protocol Buffers.
9.2 Чистый код и архитектура
Чистый код — это код, который легко читать, понимать и поддерживать. Он хорошо
структурирован, понятен и не содержит избыточных или запутанных частей.
Принципы чистого кода помогают создавать высококачественное программное
обеспечение.
9.2.1 Принципы SOLID
SOLID — это набор из пяти принципов объектно-ориентированного
программирования и дизайна, которые помогают создавать более понятный, гибкий
и поддерживаемый код.
• S - Single Responsibility Principle (Принцип единственной ответственности):
Каждый класс или модуль должен иметь только одну причину для изменения, то
есть одну ответственность.
• Пример: Класс User должен заниматься только данными пользователя, а не
отправкой email или логированием.
• O - Open/Closed Principle (Принцип открытости/закрытости): Программные
сущности (классы, модули, функции) должны быть открыты для расширения, но
закрыты для модификации.
• Пример: Вместо изменения существующего кода для добавления новой
функциональности, создайте новый модуль или класс, который расширяет
существующий.
• L - Liskov Substitution Principle (Принцип подстановки Барбары Лисков):
Объекты в программе должны быть заменяемыми экземплярами их подтипов без
изменения корректности программы.
• Пример: Если у вас есть функция, которая принимает Animal , она должна
корректно работать как с Dog , так и с Cat .
• I - Interface Segregation Principle (Принцип разделения интерфейса): Клиенты
не должны зависеть от интерфейсов, которые они не используют. Лучше иметь
много маленьких, специфичных интерфейсов, чем один большой, общий.
• Пример: Вместо одного большого интерфейса Worker с методами work() ,
eat() , sleep() , manage() , лучше иметь Workable , Eatable , Sleepable ,
Manageable .
• D - Dependency Inversion Principle (Принцип инверсии зависимостей): Модули
верхних уровней не должны зависеть от модулей нижних уровней. Оба должны
зависеть от абстракций. Абстракции не должны зависеть от деталей. Детали
должны зависеть от абстракций.
• Пример: Вместо того чтобы класс UserService напрямую зависел от конкретной
реализации MongoDBRepository , он должен зависеть от абстракции
UserRepositoryInterface .
9.2.2 DRY, KISS, YAGNI
• DRY (Don't Repeat Yourself): Избегайте дублирования кода. Если вы видите, что
повторяете один и тот же блок кода или логику, вынесите его в отдельную
функцию, модуль или компонент.
• KISS (Keep It Simple, Stupid): Делайте вещи максимально простыми. Избегайте
излишней сложности. Простое решение часто является лучшим.
• YAGNI (You Ain't Gonna Need It): Не добавляйте функциональность, которая вам
не нужна прямо сейчас. Разрабатывайте только то, что требуется в данный
момент. Это помогает избежать переусложнения и траты времени на ненужные
функции.
9.2.3 Структура проекта и модульность
Хорошая структура проекта делает его более понятным и легким для навигации и
поддержки.
• Разделение по фичам (Feature-based): Организуйте код по бизнес-функциям
(например, users , posts , comments ), а не по типам файлов ( controllers , models ,
routes ).
• Модульность: Разделяйте код на небольшие, независимые модули, каждый из
которых выполняет одну конкретную задачу. Это улучшает переиспользование
кода и упрощает тестирование.
9.3 Документация и стандарты кодирования
9.3.1 Важность документации
Хорошая документация критически важна для долгосрочного успеха проекта,
особенно в командной разработке.
• Документация кода (Code Comments): Объясняйте сложные алгоритмы,
неочевидные решения, причины, по которым был выбран тот или иной подход.
Используйте JSDoc для генерации документации API.
• Документация API (Swagger/OpenAPI): Автоматически генерируемая
документация для REST API. Позволяет разработчикам фронтенда и другим
потребителям API легко понять, как использовать ваш бэкенд.
• Swagger UI: Инструмент для визуализации и взаимодействия с API, описанным
в формате OpenAPI.
• Документация проекта (README, Wiki): Общая информация о проекте,
инструкции по установке, запуску, развертыванию, архитектурные решения,
принципы работы.
9.3.2 Стандарты кодирования (ESLint, Prettier)
Стандарты кодирования — это набор правил, которым следует команда при
написании кода. Они обеспечивают единообразие, улучшают читаемость и помогают
избежать ошибок.
• ESLint: Инструмент для статического анализа кода JavaScript. Помогает находить
проблемы в коде, такие как синтаксические ошибки, нарушения стиля,
потенциальные баги. Позволяет настроить собственные правила или
использовать популярные наборы правил (например, Airbnb, Standard).
• Установка: npm install eslint --save-dev
• Инициализация: npx eslint --init
• Запуск: npx eslint .
• Prettier: Форматтер кода. Автоматически форматирует код в соответствии с
заданными правилами, избавляя разработчиков от ручной работы по
форматированию и споров о стиле.
• Установка: npm install prettier --save-dev
• Запуск: npx prettier --write .
• Husky + lint-staged: Инструменты для автоматического запуска ESLint и Prettier
перед коммитом, чтобы гарантировать, что в репозиторий попадает только
отформатированный и проверенный код.
• Husky: Позволяет легко подключать Git-хуки.
• lint-staged: Запускает линтеры только на измененных файлах.
9.4 Интернационализация (i18n) и Локализация (l10n)
Интернационализация (i18n) — это процесс проектирования и разработки
приложения таким образом, чтобы его можно было легко адаптировать для разных
языков и регионов без изменения кода. Локализация (l10n) — это процесс адаптации
интернационализированного приложения для конкретного языка и региона.
9.4.1 Ключевые аспекты i18n/l10n
• Текстовые строки: Все пользовательские тексты должны быть вынесены в
отдельные файлы ресурсов (например, JSON-файлы) для каждого языка.
• Форматирование чисел, дат, валют: Разные регионы используют разные
форматы.
• Сортировка: Алфавитная сортировка зависит от языка.
• Изображения и медиа: Могут потребоваться разные изображения или видео для
разных культур.
• Направление текста: Слева направо (LTR) или справа налево (RTL).
9.4.2 Реализация i18n в React
Популярные библиотеки для i18n в React:
• react-i18next : Мощная и гибкая библиотека, основанная на i18next .
Пример с react-i18next :
1. Установка: npm install react-i18next i18next
2. Создание файлов локализации (например, public/locales/en/[Link] ,
public/locales/ru/[Link] ):
en/[Link] :
3. Настройка i18next (например, src/[Link] ):
4. Использование в компонентах React:
9.4.3 Реализация i18n в Express (бэкенд)
На бэкенде интернационализация может быть нужна для сообщений об ошибках,
ответов API или для рендеринга шаблонов на стороне сервера.
• i18n-node : Популярная библиотека для [Link].
Пример с i18n-node :
1. Установка: npm install i18n
2. Настройка (например, [Link] ):
9.5 Промежуточный проект: Расширение функциональности блога
Давайте применим некоторые из изученных продвинутых концепций к нашему
приложению блога.
Цель: Добавить поддержку интернационализации и улучшить структуру проекта.
Шаги:
Часть 1: Интернационализация фронтенда (React)
1. Установите react-i18next : В вашем React-приложении установите необходимые
пакеты.
2. Создайте файлы локализации: Создайте папки public/locales/en и
public/locales/ru . В каждой папке создайте [Link] .
• Перенесите все статические тексты из ваших React-компонентов в эти файлы.
• Пример: "blog_title": "My Awesome Blog" в [Link] и "blog_title": "Мой крутой блог" в
[Link] .
3. Настройте [Link] : Создайте файл src/[Link] и настройте i18next .
4. Интегрируйте в компоненты: Используйте хук useTranslation в ваших React-
компонентах для отображения переведенных текстов.
5. Добавьте переключатель языка: Создайте кнопки или выпадающий список для
смены языка в интерфейсе.
Часть 2: Улучшение структуры бэкенда (Express)
1. Рефакторинг по фичам: Переорганизуйте ваш Express API, чтобы он следовал
структуре, основанной на фичах. Например, создайте папки users , posts ,
comments , и переместите соответствующие контроллеры, модели и маршруты в
эти папки.
2. Примените принципы чистого кода: Просмотрите ваш код и постарайтесь
применить принципы DRY, KISS. Например, если у вас есть повторяющаяся логика
валидации или обработки ошибок, вынесите ее в отдельные middleware или
утилиты.
Тестирование:
• Убедитесь, что фронтенд корректно переключает языки и отображает
переведенные тексты.
• Проверьте, что бэкенд продолжает работать корректно после рефакторинга
структуры.
9.6 Практические задания по Главе 9
Для закрепления материала и развития практических навыков, выполните
следующие задания.
1. Рефакторинг существующего проекта с использованием принципов
SOLID/DRY/KISS:
• Возьмите любой из ваших предыдущих проектов (например, Todo-лист,
приложение блога) и проведите рефакторинг.
• Принцип единственной ответственности: Разделите функции или классы,
которые выполняют несколько задач, на более мелкие, сфокусированные
единицы.
• DRY: Найдите повторяющийся код и вынесите его в переиспользуемые функции
или модули.
• KISS: Упростите любую излишне сложную логику или структуру.
• Опишите, какие изменения вы внесли и как они улучшили код.
2. Настройка ESLint и Prettier в проекте:
• Выберите один из ваших проектов (фронтенд или бэкенд).
• Установите ESLint и Prettier.
• Настройте их, используя популярный набор правил (например, eslint-config-
airbnb для ESLint).
• Запустите ESLint и Prettier для вашего проекта. Исправьте все ошибки и
отформатируйте код.
• (Бонус) Настройте Husky и lint-staged для автоматического запуска ESLint и
Prettier перед коммитом.
3. Изучение и реализация Swagger/OpenAPI для Express API:
• Возьмите ваш Express API для блога.
• Исследуйте библиотеки, такие как swagger-jsdoc и swagger-ui-express , для
генерации документации OpenAPI из комментариев в вашем коде.
• Интегрируйте Swagger UI в ваш Express API, чтобы документация была доступна
по определенному маршруту (например, /api-docs ).
• Добавьте JSDoc-комментарии к вашим маршрутам и контроллерам, чтобы они
отображались в Swagger UI.
4. Интернационализация в [Link] (бэкенд):
• Добавьте поддержку интернационализации в ваш Express API, используя i18n-
node .
• Создайте файлы локализации для сообщений об ошибках или ответов API
(например, [Link] , [Link] ).
• Используйте req.__() для перевода сообщений в ответах API.
• Протестируйте, отправляя запросы с параметром ?lang=ru или кукой lang=ru .
Глава 10: Карьера Full-Stack JavaScript разработчика и
дальнейшее развитие
Поздравляем! Вы прошли полный курс по Full-Stack JavaScript разработке. Теперь у
вас есть прочная основа и практические навыки для создания полноценных веб-
приложений. Однако путь обучения в IT никогда не заканчивается. Эта
заключительная глава посвящена тому, как начать свою карьеру в качестве Full-Stack
JavaScript разработчика, как продолжать развиваться и оставаться актуальным в
быстро меняющемся мире технологий.
Мы обсудим создание портфолио, подготовку к собеседованиям, важность
непрерывного обучения и изучения новых технологий. Также будут даны
рекомендации по поиску работы и развитию в сообществе разработчиков. Помните,
что этот курс — это только начало вашего пути. Успех в карьере разработчика зависит
не только от технических знаний, но и от способности к самообучению, адаптации и
постоянному совершенствованию.
10.1 Создание портфолио
Ваше портфолио — это ваш главный инструмент для демонстрации навыков и опыта
потенциальным работодателям. Оно должно содержать реальные проекты, которые
показывают вашу способность создавать полноценные Full-Stack приложения.
Что должно быть в портфолио:
• Реальные проекты: Включите проекты, которые вы создали в рамках этого курса
(например, приложение блога, Todo-лист, e-commerce приложение). Убедитесь,
что они полностью функциональны и развернуты (на Netlify, Heroku, Vercel и т.д.).
• Разнообразие: Постарайтесь показать разнообразие навыков: разные фронтенд-
фреймворки (если изучали), разные базы данных, использование различных API,
реализация аутентификации/авторизации, тесты, CI/CD.
• Исходный код: Все проекты должны быть доступны на GitHub (или
GitLab/Bitbucket) с чистым, хорошо документированным кодом. Используйте
[Link] для каждого проекта, чтобы описать:
• Название и краткое описание проекта.
• Используемые технологии (стек).
• Как запустить проект локально.
• Ссылки на развернутую версию (live demo).
• Ключевые функции и особенности.
• Скриншоты или видео (опционально).
• Личный сайт-портфолио: Создайте простой, но профессиональный сайт, на
котором будут ссылки на ваши проекты, ваше резюме, контактная информация и
краткое описание ваших навыков.
Советы по проектам для портфолио:
• Не просто клоны: Старайтесь добавить уникальные функции или улучшить
существующие. Например, если вы делаете Todo-лист, добавьте синхронизацию с
бэкендом, аутентификацию, категории, дедлайны.
• Решайте реальные проблемы: Подумайте о небольшой проблеме, которую вы
или ваши знакомые могли бы решить с помощью веб-приложения. Это покажет
вашу способность к решению проблем.
• Участвуйте в Open Source: Внесение вклада в проекты с открытым исходным
кодом — отличный способ показать свои навыки работы в команде и
взаимодействия с чужим кодом.
10.2 Подготовка к собеседованиям
Подготовка к собеседованиям включает в себя несколько аспектов: технические
вопросы, поведенческие вопросы и демонстрация проектов.
10.2.1 Технические вопросы
• Основы JavaScript: Глубокое понимание JavaScript (ES6+, асинхронность,
замыкания, this , прототипы, Event Loop).
• Фронтенд: React (жизненный цикл компонентов, хуки, управление состоянием,
контекст, оптимизация рендеринга), HTML/CSS (семантика, доступность,
Flexbox/Grid, адаптивный дизайн).
• Бэкенд: [Link] (Event Loop, потоки, модули), [Link] (middleware,
маршрутизация), работа с базами данных (SQL/NoSQL, ORM/ODM, индексы,
транзакции).
• Общие концепции: RESTful API, HTTP-протокол, безопасность (XSS, CSRF,
инъекции), принципы SOLID, паттерны проектирования, тестирование
(модульное, интеграционное, E2E), Git.
• Алгоритмы и структуры данных: Базовые алгоритмы (сортировка, поиск),
структуры данных (массивы, связные списки, деревья, хеш-таблицы). Это
особенно важно для крупных компаний.
Ресурсы для подготовки:
• LeetCode, HackerRank: Для решения алгоритмических задач.
• "JavaScript. Подробное руководство" (Дэвид Флэнаган): Для глубокого
понимания JavaScript.
• "Вы не знаете JS" (Кайл Симпсон): Серия книг для понимания тонкостей
JavaScript.
• "Cracking the Coding Interview" (Гейл Лакман Макдауэлл): Классика для
подготовки к техническим собеседованиям.
• MDN Web Docs, freeCodeCamp, The Odin Project: Для повторения основ.
10.2.2 Поведенческие вопросы
Работодатели хотят понять, как вы работаете в команде, решаете проблемы и
справляетесь со стрессом.
• "Расскажите о себе."
• "Почему вы хотите работать у нас?"
• "Расскажите о проекте, которым вы гордитесь."
• "Опишите ситуацию, когда вы столкнулись с трудностями, и как вы их
преодолели."
• "Как вы справляетесь с критикой?"
• "Как вы учитесь новому?"
Подготовка: Используйте метод STAR (Situation, Task, Action, Result) для
структурирования своих ответов. Заранее продумайте несколько историй из своего
опыта, которые демонстрируют ваши сильные стороны.
10.2.3 Демонстрация проектов
Будьте готовы показать и объяснить код своих портфолио-проектов. Расскажите о
принятых архитектурных решениях, о трудностях, с которыми вы столкнулись, и о
том, как вы их решили. Покажите, как работает приложение, и объясните, почему вы
выбрали те или иные технологии.
10.3 Непрерывное обучение и развитие
Мир IT постоянно меняется. Чтобы оставаться востребованным специалистом,
необходимо постоянно учиться.
• Следите за новостями: Подписывайтесь на блоги, новостные рассылки,
подкасты, YouTube-каналы, посвященные JavaScript, веб-разработке, новым
фреймворкам и инструментам.
• Изучайте новые технологии: Не бойтесь пробовать новые фреймворки,
библиотеки, языки программирования. Например, TypeScript, [Link], GraphQL,
WebSockets, Serverless Functions, Deno, Svelte, [Link].
• Читайте документацию: Официальная документация — лучший источник
информации.
• Практикуйтесь: Решайте задачи, создавайте небольшие проекты, участвуйте в
хакатонах.
• Учитесь у других: Изучайте чужой код, участвуйте в code review, смотрите
выступления на конференциях.
• Делитесь знаниями: Обучение других — отличный способ закрепить свои
знания. Пишите статьи, выступайте на митапах, помогайте новичкам.
10.4 Поиск работы
• Резюме: Составьте четкое, лаконичное резюме, подчеркивающее ваши навыки и
опыт. Адаптируйте его под каждую вакансию.
• LinkedIn: Оптимизируйте свой профиль LinkedIn, добавьте проекты, навыки,
рекомендации.
• Платформы для поиска работы: HeadHunter, SuperJob, LinkedIn Jobs, Glassdoor,
Indeed, а также специализированные IT-ресурсы.
• Нетворкинг: Посещайте митапы, конференции, вебинары. Общайтесь с другими
разработчиками.
• Отклики: Откликайтесь на вакансии, которые соответствуют вашим навыкам. Не
бойтесь откликаться, даже если не соответствуете всем требованиям.
10.5 Дальнейшее развитие
После того как вы освоите основы Full-Stack JavaScript, вы можете углубиться в
различные области:
• Специализация: Стать экспертом в одной области (например, фронтенд-
разработчик, бэкенд-разработчик, DevOps-инженер, специалист по базам
данных).
• Архитектура: Изучать более сложные архитектурные паттерны (микросервисы,
бессерверные архитектуры, событийные архитектуры).
• Облачные технологии: Глубокое изучение AWS, Google Cloud, Azure.
• Мобильная разработка: React Native для создания мобильных приложений.
• Десктопная разработка: Electron для создания кроссплатформенных десктопных
приложений.
• Искусственный интеллект/Машинное обучение: Использование JavaScript для
ML ([Link]).
• Блокчейн: Разработка децентрализованных приложений (dApps).
Заключение
Вы проделали огромную работу, изучив Full-Stack JavaScript. Это мощный и
востребованный стек технологий, который открывает перед вами множество
возможностей. Продолжайте учиться, строить, экспериментировать и не бойтесь
совершать ошибки. Каждая ошибка — это возможность для роста. Удачи вам в вашей
карьере Full-Stack JavaScript разработчика!