Ответы к курсу Full-Stack JavaScript Разработки
Этот документ содержит примеры решений для практических заданий,
представленных в курсе. Помните, что в программировании часто существует
множество способов решения одной и той же задачи. Эти ответы предназначены для
того, чтобы помочь вам проверить свои собственные решения, понять различные
подходы и углубить ваше понимание материала.
Важные замечания:
• Не копируйте бездумно: Старайтесь сначала решить задачу самостоятельно.
Используйте эти ответы для проверки и обучения, а не для простого
копирования.
• Понимайте логику: Разберитесь, почему именно так написано решение, какие
концепции оно использует.
• Ищите альтернативы: Подумайте, можно ли было решить задачу по-другому, и
какой подход был бы лучше в той или иной ситуации.
• Тестируйте: Запускайте предоставленные примеры кода, чтобы убедиться в их
работоспособности.
Удачи в обучении!
Глава 1: Основы JavaScript и Node.js
Задание 1.1: Работа с переменными и типами данных
JavaScript
// chapter1_exercises.js
// 2. Объявите переменную `name` и присвойте ей ваше имя (строка).
let name = "Ваше Имя";
// 3. Объявите переменную `age` и присвойте ей ваш возраст (число).
let age = 30;
// 4. Объявите переменную `isStudent` и присвойте ей булево значение,
указывающее, являетесь ли вы студентом.
let isStudent = false;
// 5. Объявите переменную `hobbies` и присвойте ей массив из 3-5 ваших хобби.
let hobbies = ["чтение", "программирование", "путешествия", "спорт"];
// 6. Объявите переменную `address` и присвойте ей объект, содержащий
свойства `street`, `city`, `zipCode`.
let address = {
street: "Улица Программистов, 10",
city: "Кодоград",
zipCode: "123456",
};
// 7. Используя `console.log()`, выведите в консоль:
// * Тип каждой из объявленных переменных.
console.log("Тип name:", typeof name); // string
console.log("Тип age:", typeof age); // number
console.log("Тип isStudent:", typeof isStudent); // boolean
console.log("Тип hobbies:", typeof hobbies); // object (массивы в JS - это
объекты)
console.log("Тип address:", typeof address); // object
// * Значение каждой переменной.
console.log("Значение name:", name);
console.log("Значение age:", age);
console.log("Значение isStudent:", isStudent);
console.log("Значение hobbies:", hobbies);
console.log("Значение address:", address);
// * Второй элемент массива `hobbies`.
console.log("Второй элемент hobbies:", hobbies[1]); // программирование
// * Значение свойства `city` из объекта `address`.
console.log("Город из address:", address.city); // Кодоград
Задание 1.2: Условные операторы и циклы
JavaScript
// chapter1_exercises.js (продолжение)
// 2. Напишите функцию `checkAge(age)`
function checkAge(age) {
if (age < 18) {
console.log("Вы несовершеннолетний.");
} else if (age >= 18 && age <= 65) {
console.log("Вы взрослый.");
} else {
console.log("Вы пенсионер.");
}
}
// 3. Вызовите функцию `checkAge` с разными значениями возраста
console.log("\nПроверка возраста:");
checkAge(15); // Вы несовершеннолетний.
checkAge(30); // Вы взрослый.
checkAge(70); // Вы пенсионер.
// 4. Напишите цикл `for`, который выводит числа от 1 до 10
console.log("\nЧисла от 1 до 10 (for):");
for (let i = 1; i <= 10; i++) {
console.log(i);
}
// 5. Напишите цикл `while`, который выводит четные числа от 2 до 20
console.log("\nЧетные числа от 2 до 20 (while):");
let num = 2;
while (num <= 20) {
console.log(num);
num += 2;
}
Задание 1.3: Функции и стрелочные функции
JavaScript
// chapter1_exercises.js (продолжение)
// 2. Создайте обычную функцию `add(a, b)`
function add(a, b) {
return a + b;
}
// 3. Создайте стрелочную функцию `multiply(a, b)`
const multiply = (a, b) => a * b;
// 4. Создайте стрелочную функцию `greet(name)`
const greet = (name) => `Привет, ${name}!`;
// 5. Выведите результаты вызовов этих функций в консоль.
console.log("\nРезультаты функций:");
console.log("Сумма 5 и 3:", add(5, 3)); // 8
console.log("Произведение 4 и 6:", multiply(4, 6)); // 24
console.log("Приветствие:", greet("Алиса")); // Привет, Алиса!
Задание 1.4: Модули Node.js (CommonJS)
mathOperations.js
JavaScript
// mathOperations.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
chapter1_exercises.js (продолжение)
JavaScript
// chapter1_exercises.js (продолжение)
// 3. В файле `chapter1_exercises.js` импортируйте эти функции.
const { add: mathAdd, subtract: mathSubtract } = require("./mathOperations");
// 4. Используйте импортированные функции для выполнения сложения и вычитания
console.log("\nРезультаты из модулей:");
console.log("Сложение через модуль:", mathAdd(10, 5)); // 15
console.log("Вычитание через модуль:", mathSubtract(10, 5)); // 5
Задание 1.5: Работа с файловой системой (fs) и асинхронность
JavaScript
// chapter1_exercises.js (продолжение)
const fs = require("fs");
const fileName = "hello.txt";
const fileContent = "Привет, мир! Это мой первый файл.";
console.log("\nРабота с файловой системой:");
// Асинхронно запишите строку в файл `hello.txt`.
fs.writeFile(fileName, fileContent, (err) => {
if (err) {
console.error("Ошибка при записи файла:", err);
return;
}
console.log(`Файл '${fileName}' успешно записан.`);
// После успешной записи, асинхронно прочитайте содержимое файла
`hello.txt`.
fs.readFile(fileName, "utf8", (err, data) => {
if (err) {
console.error("Ошибка при чтении файла:", err);
return;
}
console.log(`Содержимое файла '${fileName}':`);
console.log(data);
});
});
Глава 2: Разработка фронтенда с React
Задание 2.1: Основы React-компонентов
my-react-app/src/WelcomeMessage.js
JavaScript
import React from 'react';
function WelcomeMessage(props) {
return (
<h1>Привет, {props.name}!</h1>
);
}
export default WelcomeMessage;
my-react-app/src/App.js
JavaScript
import React from 'react';
import WelcomeMessage from './WelcomeMessage';
import './App.css'; // Оставьте или удалите, если не используется
function App() {
return (
<div className="App">
<WelcomeMessage name="Ваше Имя" />
</div>
);
}
export default App;
Задание 2.2: Состояние (State) и обработка событий
my-react-app/src/Counter.js
JavaScript
import React, { useState } from 'react';
function Counter() {
const [count, setCount] = useState(0);
const increment = () => {
setCount(count + 1);
};
const decrement = () => {
setCount(count - 1);
};
return (
<div>
<h2>Счетчик: {count}</h2>
<button onClick={increment}>Увеличить</button>
<button onClick={decrement}>Уменьшить</button>
</div>
);
}
export default Counter;
my-react-app/src/App.js (добавьте импорт и использование Counter )
JavaScript
import React from 'react';
import WelcomeMessage from './WelcomeMessage';
import Counter from './Counter'; // Добавлено
import './App.css';
function App() {
return (
<div className="App">
<WelcomeMessage name="Ваше Имя" />
<hr />
<Counter /> {/* Добавлено */}
</div>
);
}
export default App;
Задание 2.3: Условный рендеринг и списки
my-react-app/src/ItemList.js
JavaScript
import React from 'react';
function ItemList(props) {
const { items } = props;
if (items.length === 0) {
return <p>Список пуст.</p>;
}
return (
<ul>
{items.map((item, index) => (
<li key={index}>{item}</li>
))}
</ul>
);
}
export default ItemList;
my-react-app/src/App.js (добавьте импорт и использование ItemList )
JavaScript
import React from 'react';
import WelcomeMessage from './WelcomeMessage';
import Counter from './Counter';
import ItemList from './ItemList'; // Добавлено
import './App.css';
function App() {
const fruits = ['Яблоко', 'Банан', 'Апельсин'];
const emptyList = [];
return (
<div className="App">
<WelcomeMessage name="Ваше Имя" />
<hr />
<Counter />
<hr />
<h3>Список фруктов:</h3>
<ItemList items={fruits} /> {/* Добавлено */}
<h3>Пустой список:</h3>
<ItemList items={emptyList} /> {/* Добавлено */}
</div>
);
}
export default App;
Задание 2.4: Хуки useEffect и useRef
my-react-app/src/Timer.js
JavaScript
import React, { useState, useEffect } from 'react';
function Timer() {
const [seconds, setSeconds] = useState(0);
useEffect(() => {
const intervalId = setInterval(() => {
setSeconds(prevSeconds => prevSeconds + 1);
}, 1000);
// Функция очистки: выполняется при размонтировании компонента
return () => clearInterval(intervalId);
}, []); // Пустой массив зависимостей означает, что эффект запускается один
раз при монтировании
return (
<div>
<h2>Таймер: {seconds} секунд</h2>
</div>
);
}
export default Timer;
my-react-app/src/FocusInput.js
JavaScript
import React, { useRef } from 'react';
function FocusInput() {
const inputRef = useRef(null);
const handleFocusClick = () => {
if (inputRef.current) {
inputRef.current.focus();
}
};
return (
<div>
<input type="text" ref={inputRef} placeholder="Введите что-нибудь" />
<button onClick={handleFocusClick}>Фокус на поле ввода</button>
</div>
);
}
export default FocusInput;
my-react-app/src/App.js (добавьте импорт и использование Timer и FocusInput )
JavaScript
import React from 'react';
import WelcomeMessage from './WelcomeMessage';
import Counter from './Counter';
import ItemList from './ItemList';
import Timer from './Timer'; // Добавлено
import FocusInput from './FocusInput'; // Добавлено
import './App.css';
function App() {
const fruits = ['Яблоко', 'Банан', 'Апельсин'];
const emptyList = [];
return (
<div className="App">
<WelcomeMessage name="Ваше Имя" />
<hr />
<Counter />
<hr />
<h3>Список фруктов:</h3>
<ItemList items={fruits} />
<h3>Пустой список:</h3>
<ItemList items={emptyList} />
<hr />
<Timer /> {/* Добавлено */}
<hr />
<FocusInput /> {/* Добавлено */}
</div>
);
}
export default App;
Задание 2.5: Маршрутизация с React Router
Установка: npm install react-router-dom
my-react-app/src/HomePage.js
JavaScript
import React from 'react';
function HomePage() {
return <h1>Добро пожаловать на главную страницу!</h1>;
}
export default HomePage;
my-react-app/src/AboutPage.js
JavaScript
import React from 'react';
function AboutPage() {
return <h1>О нас</h1>;
}
export default AboutPage;
my-react-app/src/ContactPage.js
JavaScript
import React from 'react';
function ContactPage() {
return <h1>Контакты</h1>;
}
export default ContactPage;
my-react-app/src/App.js (обновление для маршрутизации)
JavaScript
import React from 'react';
import { BrowserRouter as Router, Routes, Route, Link } from 'react-router-
dom';
import HomePage from './HomePage';
import AboutPage from './AboutPage';
import ContactPage from './ContactPage';
import './App.css';
function App() {
return (
<Router>
<div className="App">
<nav>
<ul>
<li>
<Link to="/">Главная</Link>
</li>
<li>
<Link to="/about">О нас</Link>
</li>
<li>
<Link to="/contact">Контакты</Link>
</li>
</ul>
</nav>
<Routes>
<Route path="/" element={<HomePage />} />
<Route path="/about" element={<AboutPage />} />
<Route path="/contact" element={<ContactPage />} />
</Routes>
</div>
</Router>
);
}
export default App;
Глава 3: Бэкенд на Node.js и Express
Задание 3.1: Создание простого Express-сервера
express-server-exercise/app.js
JavaScript
const express = require("express");
const app = express();
const port = 3000;
// Настройте маршрут GET / (корневой маршрут)
app.get("/", (req, res) => {
res.send("Привет от Express-сервера!");
});
// Запустите сервер на порту 3000
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
Как запустить:
1. Создайте папку express-server-exercise .
2. Перейдите в нее: cd express-server-exercise .
3. Инициализируйте проект: npm init -y .
4. Установите Express: npm install express .
5. Создайте файл app.js и вставьте код выше.
6. Запустите сервер: node app.js .
7. Откройте http://localhost:3000 в браузере.
Задание 3.2: Обработка различных HTTP-методов и параметров
маршрута
express-server-exercise/app.js (обновление)
JavaScript
const express = require("express");
const app = express();
const port = 3000;
// Middleware для парсинга JSON-тела запроса
app.use(express.json());
app.get("/", (req, res) => {
res.send("Привет от Express-сервера!");
});
// Маршрут GET /users/:id
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ userId: userId });
});
// Маршрут POST /products
app.post("/products", (req, res) => {
const productData = req.body;
console.log("Получены данные продукта:", productData);
res.status(201).json({ message: "Продукт добавлен:", product: productData
});
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
Как протестировать:
• GET /users/:id: Откройте http://localhost:3000/users/123 в браузере.
• POST /products: Используйте Postman, Insomnia или curl :
Задание 3.3: Промежуточное ПО (Middleware)
express-server-exercise/app.js (обновление)
JavaScript
const express = require("express");
const app = express();
const port = 3000;
app.use(express.json());
// 2. Создайте простое промежуточное ПО `loggerMiddleware`
const loggerMiddleware = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next(); // Передаем управление следующему middleware или маршруту
};
// 3. Примените `loggerMiddleware` глобально
app.use(loggerMiddleware);
app.get("/", (req, res) => {
res.send("Привет от Express-сервера!");
});
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ userId: userId });
});
app.post("/products", (req, res) => {
const productData = req.body;
console.log("Получены данные продукта:", productData);
res.status(201).json({ message: "Продукт добавлен:", product: productData
});
});
// 4. Создайте промежуточное ПО `authMiddleware`
const authMiddleware = (req, res, next) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader) {
return res.status(401).send("Неавторизованный доступ");
}
// В реальном приложении здесь была бы логика проверки токена
console.log("Заголовок Authorization присутствует.");
next();
};
// 5. Примените `authMiddleware` только к маршруту `/admin`
app.get("/admin", authMiddleware, (req, res) => {
res.send("Добро пожаловать в админ-панель!");
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
Как протестировать:
• loggerMiddleware: Просто делайте любые запросы к серверу, и вы увидите логи в
консоли.
• authMiddleware:
• Без заголовка: Откройте http://localhost:3000/admin в браузере (получите 401).
• С заголовком: Используйте Postman/Insomnia или curl :
Задание 3.4: Обработка ошибок
express-server-exercise/app.js (обновление)
JavaScript
const express = require("express");
const app = express();
const port = 3000;
app.use(express.json());
const loggerMiddleware = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
app.use(loggerMiddleware);
app.get("/", (req, res) => {
res.send("Привет от Express-сервера!");
});
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ userId: userId });
});
app.post("/products", (req, res) => {
const productData = req.body;
console.log("Получены данные продукта:", productData);
res.status(201).json({ message: "Продукт добавлен:", product: productData
});
});
const authMiddleware = (req, res, next) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader) {
return res.status(401).send("Неавторизованный доступ");
}
console.log("Заголовок Authorization присутствует.");
next();
};
app.get("/admin", authMiddleware, (req, res) => {
res.send("Добро пожаловать в админ-панель!");
});
// 2. Создайте маршрут GET /error-test, который будет генерировать ошибку
app.get("/error-test", (req, res, next) => {
// next(new Error("Что-то пошло не так!")); // Можно также передать ошибку
в next()
throw new Error("Что-то пошло не так!");
});
// 3. Создайте промежуточное ПО для обработки ошибок (error-handling
middleware)
app.use((err, req, res, next) => {
console.error(err.stack); // Выводим стек ошибки в консоль сервера для
отладки
res.status(500).send(`Ошибка сервера: ${err.message}`);
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
Как протестировать:
• Откройте http://localhost:3000/error-test в браузере. Вы должны увидеть сообщение
об ошибке 500.
Задание 3.5: Статические файлы
express-server-exercise/app.js (обновление)
JavaScript
const express = require("express");
const path = require("path"); // Импортируем модуль path
const app = express();
const port = 3000;
app.use(express.json());
const loggerMiddleware = (req, res, next) => {
console.log(`[${new Date().toISOString()}] ${req.method} ${req.url}`);
next();
};
app.use(loggerMiddleware);
// 3. В `app.js` настройте Express на отдачу статических файлов из папки
`public`
app.use(express.static(path.join(__dirname, "public")));
// Удаляем app.get("/") если index.html должен быть отдан по умолчанию
// app.get("/", (req, res) => {
// res.send("Привет от Express-сервера!");
// });
app.get("/users/:id", (req, res) => {
const userId = req.params.id;
res.json({ userId: userId });
});
app.post("/products", (req, res) => {
const productData = req.body;
console.log("Получены данные продукта:", productData);
res.status(201).json({ message: "Продукт добавлен:", product: productData
});
});
const authMiddleware = (req, res, next) => {
const authorizationHeader = req.headers.authorization;
if (!authorizationHeader) {
return res.status(401).send("Неавторизованный доступ");
}
console.log("Заголовок Authorization присутствует.");
next();
};
app.get("/admin", authMiddleware, (req, res) => {
res.send("Добро пожаловать в админ-панель!");
});
app.get("/error-test", (req, res, next) => {
throw new Error("Что-то пошло не так!");
});
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send(`Ошибка сервера: ${err.message}`);
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
express-server-exercise/public/index.html
HTML
<!DOCTYPE html>
<html lang="ru">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Моя статическая страница</title>
<link rel="stylesheet" href="/styles.css">
</head>
<body>
<h1>Моя статическая страница</h1>
<p>Это пример статического HTML-файла, отдаваемого Express.</p>
<img src="/image.png" alt="Пример изображения" style="max-width: 300px;">
<script src="/script.js"></script>
</body>
</html>
express-server-exercise/public/styles.css
CSS
body {
font-family: Arial, sans-serif;
background-color: #f0f0f0;
color: #333;
text-align: center;
padding-top: 50px;
}
h1 {
color: #0056b3;
}
express-server-exercise/public/script.js
JavaScript
console.log("Этот скрипт загружен со статической страницы!");
alert("Привет из статического JavaScript!");
Как протестировать:
1. Создайте папку public в корне вашего проекта express-server-exercise .
2. Внутри public создайте index.html , styles.css , script.js с содержимым выше.
3. (Опционально) Поместите любое изображение (например, image.png ) в папку
public .
4. Запустите сервер: node app.js .
5. Откройте http://localhost:3000 в браузере. Вы должны увидеть index.html , стили
применятся, скрипт выполнится, и изображение отобразится.
Глава 4: Работа с базами данных
Задание 4.1: Подключение к MongoDB и создание модели
(Mongoose)
mongodb-exercise/app.js
JavaScript
const mongoose = require("mongoose");
// 6. В `app.js` подключитесь к вашей базе данных MongoDB
const dbURI = "mongodb://localhost:27017/mydatabase"; // Измените, если
используете облачный сервис
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
// 7. Определите схему Mongoose для сущности `Book`
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
author: {
type: String,
required: true
},
year: Number,
genres: [String]
});
// 8. Создайте модель `Book` на основе этой схемы.
const Book = mongoose.model("Book", bookSchema);
// Экспортируем модель для использования в других файлах (для следующих
заданий)
module.exports = Book;
// Для этого задания достаточно просто запустить файл и увидеть сообщение о
подключении.
// В реальном приложении здесь был бы код для запуска сервера или выполнения
операций.
Как запустить:
1. Убедитесь, что MongoDB запущен.
2. Создайте папку mongodb-exercise .
3. Перейдите в нее: cd mongodb-exercise .
4. Инициализируйте проект: npm init -y .
5. Установите Mongoose: npm install mongoose .
6. Создайте файл app.js и вставьте код выше.
7. Запустите: node app.js . Вы должны увидеть сообщение "Успешное подключение к
MongoDB!".
Задание 4.2: CRUD-операции с Mongoose
mongodb-exercise/app.js (обновление)
JavaScript
const mongoose = require("mongoose");
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => {
console.log("Успешное подключение к MongoDB!");
performCrudOperations(); // Вызываем функцию для выполнения CRUD
})
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
author: {
type: String,
required: true
},
year: Number,
genres: [String]
});
const Book = mongoose.model("Book", bookSchema);
async function performCrudOperations() {
try {
// Очищаем коллекцию перед началом, чтобы тесты были чистыми
await Book.deleteMany({});
console.log("Коллекция Book очищена.");
// 2. Создание (Create):
console.log("\n--- Создание книги ---");
const newBook = new Book({
title: "Мастер и Маргарита",
author: "Михаил Булгаков",
year: 1967,
genres: ["Фантастика", "Сатира", "Философия"]
});
const savedBook = await newBook.save();
console.log("Сохранена книга:", savedBook);
const anotherBook = new Book({
title: "1984",
author: "Джордж Оруэлл",
year: 1949,
genres: ["Антиутопия", "Политика"]
});
await anotherBook.save();
// 3. Чтение (Read):
console.log("\n--- Чтение всех книг ---");
const allBooks = await Book.find({});
console.log("Все книги:", allBooks);
console.log("\n--- Чтение книги по названию ---");
const foundBook = await Book.findOne({ title: "Мастер и Маргарита" });
console.log("Найдена книга по названию:", foundBook);
// 4. Обновление (Update):
console.log("\n--- Обновление книги ---");
const updatedBook = await Book.findOneAndUpdate(
{ title: "Мастер и Маргарита" },
{ year: 1966 }, // Обновляем год
{ new: true } // Возвращаем обновленный документ
);
console.log("Обновленная книга:", updatedBook);
// 5. Удаление (Delete):
console.log("\n--- Удаление книги ---");
const deletedResult = await Book.deleteOne({ title: "1984" });
console.log(`Удалено документов: ${deletedResult.deletedCount}`);
// Проверяем, что книга удалена
const remainingBooks = await Book.find({});
console.log("Оставшиеся книги:", remainingBooks);
} catch (error) {
console.error("Ошибка в CRUD операциях:", error);
} finally {
// Отключаемся от базы данных после завершения операций
mongoose.connection.close();
console.log("Соединение с MongoDB закрыто.");
}
}
Как запустить:
1. Убедитесь, что MongoDB запущен.
2. Используйте app.js из предыдущего задания, заменив его содержимое на код
выше.
3. Запустите: node app.js . Вы увидите последовательный вывод операций в консоли.
Задание 4.3: Интеграция MongoDB с Express (REST API)
mongodb-exercise/app.js (обновление)
JavaScript
const express = require("express");
const mongoose = require("mongoose");
const app = express();
const port = 3000;
// Middleware для парсинга JSON-тела запроса
app.use(express.json());
// Подключение к MongoDB
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
// Определение схемы и модели Book (как в Задании 4.1)
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: true
},
author: {
type: String,
required: true
},
year: Number,
genres: [String]
});
const Book = mongoose.model("Book", bookSchema);
// --- Маршруты API для книг ---
// GET /api/books: Получить все книги.
app.get("/api/books", async (req, res) => {
try {
const books = await Book.find({});
res.json(books);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// GET /api/books/:id: Получить книгу по ID.
app.get("/api/books/:id", async (req, res) => {
try {
const book = await Book.findById(req.params.id);
if (!book) {
return res.status(404).json({ message: "Книга не найдена" });
}
res.json(book);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// POST /api/books: Создать новую книгу.
app.post("/api/books", async (req, res) => {
const book = new Book({
title: req.body.title,
author: req.body.author,
year: req.body.year,
genres: req.body.genres
});
try {
const newBook = await book.save();
res.status(201).json(newBook);
} catch (err) {
res.status(400).json({ message: err.message }); // 400 Bad Request для
ошибок валидации
}
});
// PUT /api/books/:id: Обновить книгу по ID.
app.put("/api/books/:id", async (req, res) => {
try {
const updatedBook = await Book.findByIdAndUpdate(
req.params.id,
req.body,
{ new: true, runValidators: true } // new: true возвращает обновленный
документ, runValidators: true запускает валидаторы схемы
);
if (!updatedBook) {
return res.status(404).json({ message: "Книга не найдена" });
}
res.json(updatedBook);
} catch (err) {
res.status(400).json({ message: err.message });
}
});
// DELETE /api/books/:id: Удалить книгу по ID.
app.delete("/api/books/:id", async (req, res) => {
try {
const deletedBook = await Book.findByIdAndDelete(req.params.id);
if (!deletedBook) {
return res.status(404).json({ message: "Книга не найдена" });
}
res.json({ message: "Книга успешно удалена" });
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// Обработка ошибок (общее middleware для ошибок)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Что-то пошло не так!");
});
app.listen(port, () => {
console.log(`Сервер API запущен на http://localhost:${port}`);
});
Как протестировать (используя curl или Postman/Insomnia):
1. Запустите сервер: node app.js .
2. Создание книги (POST):
3. Получение всех книг (GET):
4. Получение книги по ID (GET): (замените [book_id] на реальный ID)
5. Обновление книги по ID (PUT): (замените [book_id] на реальный ID)
6. Удаление книги по ID (DELETE): (замените [book_id] на реальный ID)
Задание 4.4: Валидация данных (Mongoose Validators)
mongodb-exercise/app.js (обновление схемы bookSchema )
JavaScript
const mongoose = require("mongoose");
const express = require("express");
const app = express();
const port = 3000;
app.use(express.json());
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: [true, "Название книги обязательно."],
minlength: [3, "Название должно быть не менее 3 символов."],
maxlength: [100, "Название не должно превышать 100 символов."]
},
author: {
type: String,
required: [true, "Автор книги обязателен."],
minlength: [3, "Имя автора должно быть не менее 3 символов."],
maxlength: [50, "Имя автора не должно превышать 50 символов."]
},
year: {
type: Number,
min: [1000, "Год издания не может быть ранее 1000 года."],
max: [new Date().getFullYear(), "Год издания не может быть в будущем."]
},
genres: {
type: [String],
validate: {
validator: function(v) {
return v == null || v.length > 0; // Разрешаем пустой массив, если он
не обязателен, или если он есть, то не пустой
},
message: props => `Массив жанров не может быть пустым, если он указан.`
}
}
});
const Book = mongoose.model("Book", bookSchema);
// ... (остальные маршруты API остаются такими же, как в Задании 4.3)
// Обновленная обработка ошибок для валидации
app.use((err, req, res, next) => {
console.error(err.stack);
if (err.name === "ValidationError") {
let errors = {};
Object.keys(err.errors).forEach((key) => {
errors[key] = err.errors[key].message;
});
return res.status(400).json({ errors });
}
res.status(500).send("Что-то пошло не так!");
});
app.listen(port, () => {
console.log(`Сервер API запущен на http://localhost:${port}`);
});
Как протестировать:
1. Запустите сервер: node app.js .
2. Попробуйте создать книгу с невалидными данными (например, слишком
короткое название, год в будущем, пустой массив жанров):
Задание 4.5: Поиск и фильтрация
mongodb-exercise/app.js (обновление маршрута GET /api/books )
JavaScript
const mongoose = require("mongoose");
const express = require("express");
const app = express();
const port = 3000;
app.use(express.json());
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
const bookSchema = new mongoose.Schema({
title: {
type: String,
required: [true, "Название книги обязательно."],
minlength: [3, "Название должно быть не менее 3 символов."],
maxlength: [100, "Название не должно превышать 100 символов."]
},
author: {
type: String,
required: [true, "Автор книги обязателен."],
minlength: [3, "Имя автора должно быть не менее 3 символов."],
maxlength: [50, "Имя автора не должно превышать 50 символов."]
},
year: {
type: Number,
min: [1000, "Год издания не может быть ранее 1000 года."],
max: [new Date().getFullYear(), "Год издания не может быть в будущем."]
},
genres: {
type: [String],
validate: {
validator: function(v) {
return v == null || v.length > 0;
},
message: props => `Массив жанров не может быть пустым, если он указан.`
}
}
});
const Book = mongoose.model("Book", bookSchema);
// GET /api/books: Получить все книги с фильтрацией.
app.get("/api/books", async (req, res) => {
try {
const { title, author, year_gte, year_lte } = req.query;
const query = {};
if (title) {
query.title = { $regex: title, $options: "i" }; // Регистронезависимый
поиск по подстроке
}
if (author) {
query.author = author; // Точное совпадение
}
if (year_gte || year_lte) {
query.year = {};
if (year_gte) {
query.year.$gte = parseInt(year_gte); // $gte: greater than or equal
}
if (year_lte) {
query.year.$lte = parseInt(year_lte); // $lte: less than or equal
}
}
const books = await Book.find(query);
res.json(books);
} catch (err) {
res.status(500).json({ message: err.message });
}
});
// ... (остальные маршруты API остаются такими же, как в Задании 4.3)
app.use((err, req, res, next) => {
console.error(err.stack);
if (err.name === "ValidationError") {
let errors = {};
Object.keys(err.errors).forEach((key) => {
errors[key] = err.errors[key].message;
});
return res.status(400).json({ errors });
}
res.status(500).send("Что-то пошло не так!");
});
app.listen(port, () => {
console.log(`Сервер API запущен на http://localhost:${port}`);
});
Как протестировать:
1. Запустите сервер: node app.js .
2. Добавьте несколько книг для тестирования фильтрации (используя POST-запрос
из Задания 4.3).
• {"title": "Приключения Тома Сойера", "author": "Марк Твен", "year": 1876, "genres":
["Приключения"]}
• {"title": "Гарри Поттер и философский камень", "author": "Джоан Роулинг", "year": 1997,
"genres": ["Фэнтези"]}
• {"title": "Мастер и Маргарита", "author": "Михаил Булгаков", "year": 1967, "genres":
["Фантастика"]}
3. Протестируйте различные комбинации параметров запроса:
• Поиск по названию: curl "http://localhost:3000/api/books?title=мастер"
• Поиск по автору: curl "http://localhost:3000/api/books?author=Марк Твен"
• Поиск по году (больше или равно): curl "http://localhost:3000/api/books?
year_gte=1900"
• Поиск по году (меньше или равно): curl "http://localhost:3000/api/books?year_lte=1990"
• Комбинированный поиск: curl "http://localhost:3000/api/books?
title=поттер&year_gte=1995"
Глава 5: Тестирование приложений
Задание 5.1: Юнит-тестирование функций (Jest)
testing-exercise/math.js
JavaScript
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
function multiply(a, b) {
return a * b;
}
function divide(a, b) {
if (b === 0) {
throw new Error("Деление на ноль невозможно");
}
return a / b;
}
module.exports = { add, subtract, multiply, divide };
testing-exercise/math.test.js
JavaScript
const { add, subtract, multiply, divide } = require("./math");
describe("Математические операции", () => {
test("функция add должна правильно складывать два числа", () => {
expect(add(1, 2)).toBe(3);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test("функция subtract должна правильно вычитать два числа", () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(3, 5)).toBe(-2);
expect(subtract(10, 0)).toBe(10);
});
test("функция multiply должна правильно умножать два числа", () => {
expect(multiply(2, 3)).toBe(6);
expect(multiply(-2, 4)).toBe(-8);
expect(multiply(5, 0)).toBe(0);
});
test("функция divide должна правильно делить два числа", () => {
expect(divide(10, 2)).toBe(5);
expect(divide(7, 2)).toBe(3.5);
});
test("функция divide должна выбрасывать ошибку при делении на ноль", () =>
{
expect(() => divide(10, 0)).toThrow("Деление на ноль невозможно");
});
});
Как запустить:
1. Создайте папку testing-exercise .
2. Перейдите в нее: cd testing-exercise .
3. Инициализируйте проект: npm init -y .
4. Установите Jest: npm install --save-dev jest .
5. Добавьте в package.json скрипт: "test": "jest" .
6. Создайте файлы math.js и math.test.js с содержимым выше.
7. Запустите тесты: npm test .
Задание 5.2: Тестирование React-компонентов (React Testing
Library)
my-react-app/src/Button.js
JavaScript
import React from 'react';
function Button({ onClick, children }) {
return (
<button onClick={onClick}>
{children}
</button>
);
}
export default Button;
my-react-app/src/Button.test.js
JavaScript
import React from 'react';
import { render, screen, fireEvent } from '@testing-library/react';
import Button from './Button';
import '@testing-library/jest-dom'; // Для расширенных матчеров, таких как
.toBeInTheDocument()
describe('Button', () => {
test('рендерится с правильным текстом', () => {
render(<Button onClick={() => {}}>Нажми меня</Button>);
const buttonElement = screen.getByText(/Нажми меня/i);
expect(buttonElement).toBeInTheDocument();
});
test('вызывает функцию onClick при клике', () => {
const handleClick = jest.fn(); // Создаем мок-функцию
render(<Button onClick={handleClick}>Кликни</Button>);
const buttonElement = screen.getByText(/Кликни/i);
fireEvent.click(buttonElement);
expect(handleClick).toHaveBeenCalledTimes(1); // Проверяем, что мок-
функция была вызвана один раз
});
});
Как запустить:
1. В вашем React-приложении (из Главы 2) или новом проекте React.
2. Установите необходимые библиотеки: npm install --save-dev @testing-library/react
@testing-library/jest-dom .
3. Создайте файлы Button.js и Button.test.js в папке src .
4. Запустите тесты: npm test (Jest уже настроен в create-react-app ).
Задание 5.3: Интеграционное тестирование Express API (Supertest)
Предполагается, что у вас есть app.js из Задания 3.2 или 4.3.
express-server-exercise/app.js (если используете для тестирования, убедитесь, что он
экспортирует app )
JavaScript
// ... (ваш код Express app.js)
module.exports = app; // Добавьте эту строку в конце файла app.js
express-server-exercise/app.test.js
JavaScript
const request = require('supertest');
const app = require('./app'); // Импортируем ваше Express-приложение
describe('Express API Tests', () => {
test('GET / должен возвращать
Привет от Express-сервера!", async () => {
const response = await request(app).get("/");
expect(response.statusCode).toBe(200);
expect(response.text).toBe("Привет от Express-сервера!");
});
test("POST /products должен создавать новый продукт", async () => {
const newProduct = { name: "Клавиатура", price: 75 };
const response = await request(app)
.post("/products")
.send(newProduct)
.expect(201);
expect(response.body.message).toBe("Продукт добавлен:");
expect(response.body.product).toEqual(newProduct);
});
// Пример теста для API с базой данных (если вы используете Mongoose из
Главы 4)
// Для этого теста потребуется настроить тестовую базу данных или
мокировать Mongoose
/*
const mongoose = require("mongoose");
const Book = require("./models/Book"); // Предполагается, что у вас есть
модель Book
beforeAll(async () => {
// Подключение к тестовой базе данных
await mongoose.connect("mongodb://localhost:27017/testdb", {
useNewUrlParser: true, useUnifiedTopology: true });
});
afterEach(async () => {
// Очистка коллекции после каждого теста
await Book.deleteMany({});
});
afterAll(async () => {
// Отключение от базы данных после всех тестов
await mongoose.connection.close();
});
test("GET /api/books должен возвращать все книги", async () => {
await Book.create({ title: "Test Book 1", author: "Author A", year: 2000
});
await Book.create({ title: "Test Book 2", author: "Author B", year: 2005
});
const response = await request(app).get("/api/books");
expect(response.statusCode).toBe(200);
expect(response.body.length).toBe(2);
expect(response.body[0].title).toBe("Test Book 1");
});
test("POST /api/books должен создавать новую книгу", async () => {
const newBook = { title: "New Test Book", author: "New Author", year:
2020 };
const response = await request(app)
.post("/api/books")
.send(newBook)
.expect(201);
expect(response.body.title).toBe("New Test Book");
const bookInDb = await Book.findOne({ title: "New Test Book" });
expect(bookInDb).toBeDefined();
});
*/
});
Как запустить:
1. В вашем Express-проекте (из Главы 3 или 4).
2. Установите Supertest: npm install --save-dev supertest .
3. Убедитесь, что ваш app.js экспортирует app (добавьте module.exports = app; в
конце файла).
4. Создайте файл app.test.js с содержимым выше.
5. Запустите тесты: npm test (если у вас настроен скрипт "test": "jest" или "test":
"mocha" с соответствующей конфигурацией).
Задание 5.4: Мокирование зависимостей
testing-exercise/bookService.js
JavaScript
// bookService.js
const Book = require("./models/Book"); // Предполагается, что у вас есть
модель Book
async function getAllBooks() {
return await Book.find({});
}
async function createBook(bookData) {
const book = new Book(bookData);
return await book.save();
}
module.exports = { getAllBooks, createBook };
testing-exercise/models/Book.js (простой мок для примера, в реальном проекте это была
бы Mongoose модель)
JavaScript
// models/Book.js
// Это простой мок для демонстрации, в реальном приложении здесь была бы
Mongoose модель
class Book {
constructor(data) {
this.data = data;
}
async save() {
// Имитация сохранения в БД
return { ...this.data, _id: "mockId" + Math.random() };
}
static async find(query) {
// Имитация поиска в БД
return [
{ _id: "1", title: "Mock Book 1", author: "Mock Author A", year: 2000
},
{ _id: "2", title: "Mock Book 2", author: "Mock Author B", year: 2005
},
];
}
}
module.exports = Book;
testing-exercise/bookService.test.js
JavaScript
const bookService = require("./bookService");
const Book = require("./models/Book"); // Импортируем мок модели Book
// Мокируем всю модель Book
jest.mock("./models/Book");
describe("bookService", () => {
beforeEach(() => {
// Очищаем все моки перед каждым тестом
jest.clearAllMocks();
});
test("getAllBooks должен возвращать все книги", async () => {
// Настраиваем мок Book.find
Book.find.mockResolvedValueOnce([
{ _id: "mock1", title: "Mocked Book 1" },
{ _id: "mock2", title: "Mocked Book 2" },
]);
const books = await bookService.getAllBooks();
expect(books).toEqual([
{ _id: "mock1", title: "Mocked Book 1" },
{ _id: "mock2", title: "Mocked Book 2" },
]);
expect(Book.find).toHaveBeenCalledTimes(1);
});
test("createBook должен создавать новую книгу", async () => {
const newBookData = { title: "New Mock Book", author: "Mock Author" };
// Настраиваем мок для экземпляра Book и его метода save
Book.mockImplementationOnce(() => ({
save: jest.fn().mockResolvedValueOnce({ ...newBookData, _id:
"newMockId" }),
}));
const createdBook = await bookService.createBook(newBookData);
expect(createdBook).toEqual({ ...newBookData, _id: "newMockId" });
expect(Book).toHaveBeenCalledWith(newBookData); // Проверяем, что
конструктор был вызван с правильными данными
expect(Book().save).toHaveBeenCalledTimes(1); // Проверяем, что метод
save был вызван
});
});
Как запустить:
1. Создайте папку testing-exercise .
2. В ней создайте папку models .
3. Создайте файлы bookService.js и models/Book.js с содержимым выше.
4. Создайте файл bookService.test.js с содержимым выше.
5. Убедитесь, что Jest установлен и настроен ( npm install --save-dev jest , скрипт "test":
"jest" ).
6. Запустите тесты: npm test .
Задание 5.5: E2E-тестирование (Cypress или Playwright -
теоретически)
Ответ:
1. Что такое E2E-тестирование и его роль:
• E2E (End-to-End) тестирование — это метод тестирования программного
обеспечения, который проверяет весь поток приложения от начала до конца,
имитируя реальное взаимодействие пользователя с системой. Оно включает в
себя тестирование всех слоев приложения: фронтенда, бэкенда, базы данных,
сетевых запросов и внешних сервисов.
• Роль в Full-Stack разработке: E2E-тесты критически важны для Full-Stack
приложений, поскольку они гарантируют, что все части системы (фронтенд,
бэкенд, база данных, API-интеграции) работают вместе как единое целое. Они
помогают выявить проблемы, которые невозможно обнаружить с помощью
юнит- или интеграционных тестов, такие как проблемы с интеграцией между
сервисами, неправильная конфигурация окружения, ошибки в
пользовательском интерфейсе, связанные с реальным поведением браузера.
2. Сценарии для E2E-тестов:
Для Full-Stack приложения с React-фронтендом и Express/Node.js бэкендом я бы
покрыл следующие сценарии:
• Регистрация нового пользователя: Проверка всего процесса от заполнения
формы регистрации на фронтенде до успешного создания пользователя в базе
данных через бэкенд.
• Вход пользователя и доступ к защищенным ресурсам: Проверка корректного
входа, получения JWT-токена и успешного доступа к страницам или API-
маршрутам, требующим аутентификации.
• Добавление/редактирование/удаление элемента (например, книги):
Полный цикл CRUD-операций: пользователь вводит данные на фронтенде, они
отправляются на бэкенд, сохраняются в БД, затем отображаются на фронтенде,
редактируются и удаляются.
• Поиск и фильтрация данных: Пользователь вводит поисковый запрос или
применяет фильтры на фронтенде, запрос отправляется на бэкенд, данные
фильтруются в БД и корректно отображаются на фронтенде.
• Обработка ошибок: Проверка поведения приложения при различных ошибках
(например, неверные входные данные, недоступность бэкенда, ошибки
сервера).
• Навигация по приложению: Проверка корректной работы всех ссылок и
маршрутов.
3. Пример тестовых шагов (Сценарий: Добавление новой книги):
Инструмент: Cypress
4. Отличие от юнит- и интеграционных тестов:
• Юнит-тесты: Тестируют наименьшие изолированные части кода (функции,
методы, компоненты) в изоляции от внешних зависимостей. Фокус на
корректности отдельных логических блоков.
• Интеграционные тесты: Проверяют взаимодействие между несколькими
модулями или компонентами. Например, как React-компонент взаимодействует
с хуками, или как Express-маршрут взаимодействует с моделью базы данных.
Они могут мокировать внешние системы (например, реальную БД).
• E2E-тесты: Тестируют всю систему как единое целое, от пользовательского
интерфейса до базы данных и внешних API. Они имитируют реальный
пользовательский сценарий и проверяют, что весь поток работает корректно.
E2E-тесты не мокируют внутренние части приложения, а взаимодействуют с
ним так, как это делал бы реальный пользователь. Они медленнее и более
хрупкие, но дают наибольшую уверенность в работоспособности всего
приложения.
Глава 6: Развертывание и обслуживание
Задание 6.1: Развертывание статического фронтенда (Netlify/Vercel
- теоретически)
Ответ:
Развертывание статического React-приложения на платформах, таких как Netlify или
Vercel, — это простой и быстрый процесс, который позволяет сделать ваше
приложение доступным в интернете за считанные минуты.
Пошаговый процесс развертывания на Netlify:
1. Подготовка проекта:
• Убедитесь, что ваше React-приложение готово к сборке. Запустите локально
команду npm run build . Эта команда создаст папку build (или dist ) с
оптимизированными статическими файлами (HTML, CSS, JS).
• Загрузите ваш проект в Git-репозиторий (GitHub, GitLab, Bitbucket). Это
необходимо для автоматического развертывания.
2. Регистрация и создание нового сайта на Netlify:
• Зарегистрируйтесь на Netlify с помощью вашего Git-провайдера (например,
GitHub).
• На главной панели нажмите "New site from Git".
• Выберите вашего Git-провайдера и авторизуйтесь.
• Выберите репозиторий с вашим React-приложением.
3. Настройка параметров сборки:
• Netlify автоматически определит, что это React-приложение, и предложит
стандартные настройки. Вам нужно проверить и, если необходимо, изменить
следующие параметры:
• Branch to deploy: Выберите ветку, из которой будет происходить
развертывание (обычно main или master ).
• Build command: Команда для сборки вашего приложения. Для create-react-
app это npm run build или CI=false npm run build (если есть предупреждения,
которые вы хотите проигнорировать).
• Publish directory: Папка, в которую помещаются результаты сборки. Для
create-react-app это build .
4. Развертывание:
• Нажмите кнопку "Deploy site".
• Netlify начнет процесс сборки и развертывания. Вы сможете наблюдать за
логами в реальном времени.
• После успешного завершения Netlify предоставит вам уникальный URL
(например, random-name-12345.netlify.app ), по которому ваше приложение будет
доступно.
5. Настройка автоматического развертывания (CI/CD):
• Этот шаг настраивается автоматически при создании сайта из Git. Netlify
добавляет веб-хук в ваш репозиторий.
• Теперь при каждом git push в выбранную ветку (например, main ) Netlify будет
автоматически запускать новую сборку и развертывание, обновляя ваше
приложение до последней версии. Это и есть непрерывное развертывание
(Continuous Deployment).
Процесс на Vercel очень похож: вы также подключаете свой Git-репозиторий, Vercel
автоматически определяет фреймворк (React) и предлагает настройки сборки, а
затем развертывает приложение и настраивает CI/CD.
Задание 6.2: Развертывание Node.js бэкенда (Heroku/Render -
теоретически)
Ответ:
Развертывание Node.js бэкенда на PaaS-платформах (Platform as a Service), таких как
Render или Heroku, позволяет абстрагироваться от управления серверами и
сосредоточиться на коде.
Пошаговый процесс развертывания на Render:
1. Подготовка проекта:
• Динамический порт: Ваш Express-сервер должен слушать порт, который
предоставляет платформа. Нельзя жестко задавать порт (например, 3000 ).
Вместо этого используйте process.env.PORT :
• Стартовый скрипт: В package.json должен быть скрипт start , который
запускает ваше приложение. Render будет использовать его по умолчанию.
• Загрузка в Git: Загрузите ваш проект в Git-репозиторий (GitHub, GitLab).
2. Регистрация и создание нового сервиса на Render:
• Зарегистрируйтесь на Render с помощью вашего Git-провайдера.
• На панели управления нажмите "New +" и выберите "Web Service".
• Подключите ваш Git-репозиторий и выберите нужный проект.
3. Настройка параметров сервиса:
• Name: Придумайте уникальное имя для вашего сервиса.
• Region: Выберите регион, ближайший к вашей аудитории.
• Branch: Выберите ветку для развертывания (например, main ).
• Root Directory: Оставьте пустым, если package.json находится в корне проекта.
• Runtime: Render автоматически определит Node .
• Build Command: npm install (или yarn install ).
• Start Command: npm start (или node app.js ).
4. Настройка переменных окружения (Environment Variables):
• Это критически важный шаг для бэкенда. Здесь вы должны указать все
секретные данные, которые не хранятся в коде.
• Нажмите "Advanced" или найдите раздел "Environment".
• Добавьте переменные, такие как:
• DATABASE_URL : Строка подключения к вашей облачной базе данных
(например, MongoDB Atlas, ElephantSQL для PostgreSQL).
• JWT_SECRET : Секретный ключ для подписи JWT-токенов.
• NODE_ENV : production .
5. Развертывание:
• Нажмите "Create Web Service".
• Render начнет установку зависимостей, сборку (если есть) и запуск вашего
приложения.
• После успешного развертывания Render предоставит вам URL (например, my-
backend-app.onrender.com ), по которому будет доступен ваш API.
6. Доступность API:
• Ваш API будет доступен по предоставленному URL. Например, если у вас был
маршрут /api/books , то после развертывания он будет доступен по адресу
https://my-backend-app.onrender.com/api/books .
• Не забудьте обновить URL бэкенда в вашем фронтенд-приложении перед его
развертыванием.
Процесс на Heroku аналогичен, но требует наличия Procfile в корне проекта,
который указывает, как запускать приложение (например, web: npm start ).
Задание 6.3: Использование Docker для контейнеризации
(теоретически)
Ответ:
1. Что такое Docker и зачем он нужен:
• Docker — это платформа для разработки, доставки и запуска приложений в
изолированных средах, называемых контейнерами. Контейнер включает в
себя все необходимое для работы приложения: код, среду выполнения
(например, Node.js), системные инструменты, библиотеки и настройки. Это
гарантирует, что приложение будет работать одинаково в любой среде — на
компьютере разработчика, на тестовом сервере или в продакшене.
• Зачем нужен в Full-Stack разработке:
• Решение проблемы "у меня на машине все работает": Docker устраняет
различия между окружениями, гарантируя консистентность.
• Изоляция: Приложения и их зависимости изолированы друг от друга, что
предотвращает конфликты.
• Портативность: Контейнеры можно легко переносить и запускать на любой
системе, где установлен Docker.
• Быстрое развертывание: Docker упрощает и ускоряет процесс
развертывания.
• Микросервисы: Docker идеально подходит для создания и управления
микросервисной архитектурой, где каждое приложение работает в своем
контейнере.
2. Пример Dockerfile для Node.js бэкенда:
3. Команды Docker для сборки и запуска:
• Сборка образа: Эта команда собирает Docker-образ на основе Dockerfile и
присваивает ему имя (тег) my-node-app .
• Запуск контейнера: Эта команда запускает контейнер из образа my-node-app .
Флаг -d запускает контейнер в фоновом режиме. Флаг -p 8080:3000
пробрасывает порт 8080 с хост-машины на порт 3000 внутри контейнера.
4. Как Docker помогает в разработке, тестировании и развертывании:
• Разработка: Разработчики могут быстро поднять всю необходимую
инфраструктуру (базу данных, Redis, бэкенд, фронтенд) с помощью docker-
compose , не устанавливая все это на свою машину.
• Тестирование: CI/CD пайплайны могут использовать Docker для создания
чистого, изолированного окружения для каждого запуска тестов, что
гарантирует их надежность и воспроизводимость.
• Развертывание: Собранный Docker-образ можно развернуть на любой
облачной платформе, поддерживающей контейнеры (AWS ECS, Google
Kubernetes Engine, Azure Container Instances, DigitalOcean), что делает процесс
развертывания универсальным и предсказуемым.
Задание 6.4: Мониторинг и логирование (теоретически)
Ответ:
1. Важность мониторинга и логирования:
• Мониторинг — это процесс сбора и анализа метрик производительности и
доступности приложения в реальном времени. Он позволяет проактивно
выявлять проблемы (например, утечки памяти, высокую нагрузку на ЦП,
медленные запросы к БД) до того, как они повлияют на пользователей.
• Логирование — это процесс записи событий, происходящих в приложении.
Логи критически важны для отладки проблем в продакшене, анализа
поведения пользователей, аудита безопасности и понимания, что именно
произошло в момент сбоя.
• Без мониторинга и логирования работа с приложением в продакшене похожа
на полет вслепую. Вы не знаете, работает ли оно корректно, с какими
проблемами сталкиваются пользователи и как устранить возникающие
ошибки.
2. Типы метрик для отслеживания:
• Фронтенд:
• Web Vitals (LCP, FID, CLS): Ключевые метрики пользовательского опыта.
• Время загрузки страницы: Как быстро страница становится интерактивной.
• Частота ошибок JavaScript: Сколько ошибок происходит в браузерах
пользователей.
• Производительность API-запросов: Время ответа на запросы к бэкенду с
точки зрения клиента.
• Бэкенд:
• Время ответа API (Latency): Среднее и 95/99-процентильное время ответа на
запросы.
• Пропускная способность (Throughput): Количество запросов в секунду
(RPS).
• Частота ошибок сервера (5xx): Количество серверных ошибок.
• Использование ресурсов: Нагрузка на ЦП, использование памяти, дисковое
пространство.
• Производительность базы данных: Время выполнения запросов,
количество медленных запросов.
3. Инструменты для логирования и мониторинга:
• Логирование:
• Winston / Pino: Популярные библиотеки для структурированного
логирования в Node.js. Они позволяют записывать логи в разных форматах
(JSON) и отправлять их в различные хранилища.
• ELK Stack (Elasticsearch, Logstash, Kibana) / Loki: Системы для
централизованного сбора, хранения и анализа логов со всех сервисов.
• Мониторинг и оповещение:
• Prometheus + Grafana: Стандарт де-факто для сбора метрик и их
визуализации. Prometheus собирает метрики, а Grafana позволяет создавать
красивые и информативные дашборды.
• Sentry / Bugsnag: Сервисы для отслеживания ошибок в реальном времени.
Они автоматически собирают ошибки с фронтенда и бэкенда, группируют их
и присылают уведомления.
• New Relic / Datadog: Комплексные APM (Application Performance Monitoring)
платформы, которые предоставляют детальную информацию о
производительности всего стека.
4. Настройка сбора и анализа логов:
1. Интеграция библиотеки логирования: В Node.js приложении интегрировать
Winston или Pino. Настроить разные уровни логирования ( info , warn , error ).
2. Структурированные логи: Настроить вывод логов в формате JSON. Каждый
лог должен содержать полезную информацию: временную метку, уровень лога,
сообщение, а также контекст (ID запроса, ID пользователя и т.д.).
3. Централизованный сбор: Настроить отправку логов из приложения в
централизованную систему (например, Logstash, Loki или облачный сервис).
Обычно это делается путем вывода логов в stdout в контейнере, а система
сбора логов (например, Fluentd) сама забирает их и отправляет в хранилище.
4. Анализ и оповещение: В системе анализа логов (Kibana, Grafana Loki)
настроить дашборды для визуализации логов и оповещения (alerts) о
критических ошибках (например, если количество ошибок уровня error
превышает определенный порог).
Задание 6.5: Обновление и поддержка приложений (теоретически)
Ответ:
1. Процесс выпуска обновлений:
Процесс выпуска обновлений для Full-Stack приложения должен быть
автоматизированным и безопасным. Обычно он включает следующие шаги:
1. Разработка в ветках: Новые функции и исправления разрабатываются в
отдельных feature-ветках в Git.
2. Код-ревью и слияние: После завершения разработки создается Pull Request
(или Merge Request). Код проходит ревью коллегами, после чего сливается в
основную ветку разработки (например, develop или main ).
3. Автоматизированное тестирование: CI/CD пайплайн автоматически запускает
все тесты (юнит, интеграционные, E2E) для слитого кода.
4. Сборка артефактов: Если тесты проходят, CI/CD система собирает артефакты:
Docker-образ для бэкенда и статические файлы для фронтенда.
5. Развертывание на Staging: Артефакты развертываются на промежуточном
(staging) окружении, которое является точной копией продакшена. Здесь
проводится ручное тестирование и проверка новой функциональности.
6. Развертывание на Production: После успешного тестирования на staging,
обновление развертывается на продакшн с использованием одной из
безопасных стратегий.
2. Стратегии развертывания:
• Blue/Green Deployment (Сине-зеленое развертывание): У вас есть два
идентичных продакшн-окружения: "синее" (текущая версия) и "зеленое" (новая
версия). Трафик переключается с синего на зеленое. Если что-то идет не так,
трафик можно мгновенно переключить обратно. Это безопасно, но требует
двойных ресурсов.
• Canary Deployment (Канареечное развертывание): Новая версия сначала
выкатывается на небольшую часть пользователей (например, 1%). Если
мониторинг не показывает проблем, процент пользователей постепенно
увеличивается до 100%. Это позволяет выявить проблемы на ранней стадии с
минимальным влиянием на пользователей.
• Rolling Update (Постепенное обновление): Обновление происходит
постепенно, инстанс за инстансом. Например, если у вас 10 серверов, вы
обновляете их по одному, пока все не будут обновлены. Это стандартная
стратегия во многих системах (например, Kubernetes).
3. Обработка обратной совместимости API:
Это критически важно, чтобы не сломать работающие клиенты (фронтенд,
мобильные приложения). Основные правила:
• Никогда не удаляйте и не переименовывайте поля: Вместо этого помечайте
их как устаревшие ( deprecated ) и добавляйте новые.
• Не изменяйте типы данных полей.
• Добавляйте только опциональные поля: Новые обязательные поля в
запросах сломают старые клиенты.
• Версионирование API: Если требуются ломающие изменения, создайте новую
версию API (например, /api/v2/books ). Старая версия ( /api/v1/books ) должна
продолжать работать какое-то время.
4. Важность регулярного обновления зависимостей:
• Безопасность: Зависимости могут содержать уязвимости. Регулярное
обновление (например, с помощью npm audit и npm update ) помогает
устанавливать патчи безопасности и защищать приложение от атак.
• Исправление ошибок: Новые версии библиотек содержат исправления
ошибок, которые могут влиять на ваше приложение.
• Новые функции и производительность: Обновления часто приносят новые
возможности и улучшения производительности.
• Технический долг: Если долго не обновлять зависимости, разрыв между
версиями становится слишком большим, и будущее обновление может
превратиться в очень сложную и рискованную задачу.
Глава 7: Безопасность Full-Stack приложений
Задание 7.1: Защита от XSS-атак
Ответ:
1. Что такое XSS (Cross-Site Scripting) атака и как она работает:
• XSS — это тип уязвимости веб-безопасности, который позволяет
злоумышленнику внедрять вредоносный клиентский скрипт (обычно
JavaScript) в веб-страницы, просматриваемые другими пользователями. Когда
жертва открывает зараженную страницу, вредоносный скрипт выполняется в
ее браузере, получая доступ к данным пользователя (например, куки, токены
сессии), которые обычно защищены политикой Same-Origin Policy.
• Как работает: Злоумышленник находит уязвимое место на сайте, где
пользовательский ввод не санитаризируется должным образом перед
отображением. Например, поле для комментариев, поисковая строка или
профиль пользователя. Он вводит туда вредоносный скрипт. Когда другой
пользователь просматривает эту страницу, браузер выполняет внедренный
скрипт, считая его частью легитимного контента сайта.
2. Пример уязвимого кода:
Представьте, что у вас есть Express-сервер, который просто отображает имя
пользователя, полученное из URL-параметра, без какой-либо санитаризации:
3. Как предотвратить XSS-атаки:
• На фронтенде (React):
• Экранирование (Escaping) / Санитаризация (Sanitization): React по
умолчанию экранирует содержимое, которое вы вставляете в JSX, что
предотвращает XSS. Например, <p>{someUserGeneratedContent}</p> будет
безопасным. Однако, если вы используете dangerouslySetInnerHTML , вы
должны вручную санитаризировать контент.
• Библиотеки для санитаризации: Используйте библиотеки, такие как
DOMPurify , для очистки HTML-строк, полученных от пользователей, прежде
чем вставлять их в DOM (особенно при использовании
dangerouslySetInnerHTML ).
• Content Security Policy (CSP): Настройте HTTP-заголовок Content-Security-
Policy на сервере. CSP позволяет браузеру знать, какие источники контента
(скрипты, стили, изображения) разрешено загружать и выполнять. Это
может предотвратить выполнение инжектированных скриптов, если они не
соответствуют разрешенным источникам.
• На бэкенде:
• Валидация и санитаризация ввода: Всегда валидируйте и санитаризируйте
любой пользовательский ввод, прежде чем сохранять его в базе данных или
отображать. Удаляйте или экранируйте потенциально вредоносные символы
(например, < , > , script ).
• Кодирование вывода (Output Encoding): Перед отправкой данных клиенту,
убедитесь, что все данные, полученные от пользователя, правильно
закодированы для контекста, в котором они будут отображаться (HTML-
кодирование, URL-кодирование и т.д.).
4. Библиотеки для санитаризации пользовательского ввода:
• DOMPurify (для фронтенда): Очень надежная и широко используемая
библиотека для очистки HTML от потенциально вредоносного кода.
• xss (для Node.js): Библиотека для Node.js, которая позволяет фильтровать
HTML-строки, удаляя или экранируя опасные теги и атрибуты.
• validator.js (для Node.js): Хотя в основном используется для валидации, имеет
функции для санитаризации строк, например, escape() .
Задание 7.2: Защита от CSRF-атак
Ответ:
1. Что такое CSRF (Cross-Site Request Forgery) атака и как она работает:
• CSRF (также известная как XSRF) — это тип атаки, при которой злоумышленник
заставляет аутентифицированного пользователя выполнить нежелательное
действие на веб-сайте, на котором пользователь уже вошел в систему. Атака
использует доверие сайта к браузеру пользователя.
• Как работает: Злоумышленник создает вредоносную веб-страницу (или
внедряет вредоносный код на легитимный сайт), которая содержит скрытый
запрос к уязвимому сайту. Например, это может быть форма, которая
автоматически отправляется, или изображение, которое на самом деле
является запросом на изменение пароля. Если пользователь, вошедший в
систему на уязвимом сайте, посещает вредоносную страницу, его браузер
автоматически отправит запрос вместе с куками сессии, и сайт воспримет его
как легитимный запрос от пользователя.
2. Пример сценария CSRF-атаки:
Представьте, что банк имеет URL для перевода денег: GET http://bank.com/transfer?
account=злоумышленник&amount=1000 . Если пользователь вошел в свой банковский
аккаунт, злоумышленник может отправить ему электронное письмо с
изображением, URL которого выглядит так:
3. Основные методы защиты от CSRF-атак:
• CSRF-токены (Synchronizer Token Pattern): Самый распространенный и
эффективный метод. Сервер генерирует уникальный, непредсказуемый токен
для каждой пользовательской сессии. Этот токен включается в каждую форму
или AJAX-запрос, который изменяет состояние на сервере. Сервер проверяет
наличие и валидность токена при каждом запросе. Поскольку злоумышленник
не может получить этот токен (из-за Same-Origin Policy), он не может подделать
запрос.
• SameSite-куки: Атрибут SameSite для куки позволяет браузеру
контролировать, когда куки должны отправляться с межсайтовыми запросами.
Значения Lax (по умолчанию для многих браузеров) или Strict могут
значительно снизить риск CSRF, предотвращая отправку куки с кросс-
сайтовыми запросами.
• Проверка заголовка Referer или Origin : Сервер может проверять заголовки
Referer (откуда пришел запрос) или Origin (домен, с которого был
инициирован запрос), чтобы убедиться, что запрос исходит от ожидаемого
домена. Однако эти заголовки могут быть подделаны или отсутствовать в
некоторых случаях.
• Двойная отправка куки (Double Submit Cookie): Менее безопасный, но иногда
используемый метод, когда токен хранится как в куки, так и в скрытом поле
формы. Сервер проверяет, что оба значения совпадают.
4. Реализация защиты от CSRF в Express-приложении:
Для Express-приложений часто используется библиотека csurf .
1. Установка: npm install csurf cookie-parser
2. Использование в app.js :
Задание 7.3: Защита от SQL-инъекций (для баз данных)
Ответ:
1. Что такое SQL-инъекция и как она может быть использована
злоумышленником:
• SQL-инъекция — это уязвимость веб-безопасности, которая позволяет
злоумышленнику вмешиваться в запросы, которые приложение делает к своей
базе данных. Это происходит, когда приложение встраивает пользовательский
ввод непосредственно в SQL-запросы без должной валидации или
экранирования.
• Как используется: Злоумышленник вводит специально сформированные
строки в поля ввода приложения (например, логин, пароль, поисковая строка),
которые изменяют логику SQL-запроса. Это может позволить ему:
• Получить доступ к конфиденциальным данным (пароли, личная
информация).
• Изменять или удалять данные в базе данных.
• Выполнять административные команды на сервере базы данных.
• Обойти аутентификацию.
2. Пример уязвимого SQL-запроса:
Представьте, что у вас есть код, который формирует SQL-запрос для входа
пользователя:
3. Как предотвратить SQL-инъекции:
• Параметризованные запросы (Prepared Statements): Это основной и
наиболее эффективный метод. Вместо того чтобы вставлять пользовательский
ввод непосредственно в строку запроса, вы используете заполнители
(placeholders) для значений. База данных обрабатывает запрос и данные
отдельно, гарантируя, что ввод пользователя будет рассматриваться как
данные, а не как часть SQL-кода.
• ORM (Object-Relational Mappers) и ODM (Object-Document Mappers): Такие
библиотеки, как Sequelize (для SQL) или Mongoose (для MongoDB), по
умолчанию используют параметризованные запросы или эквивалентные
механизмы для безопасной работы с данными. Они абстрагируют вас от
прямого написания SQL и автоматически экранируют пользовательский ввод.
• Валидация и очистка ввода: Хотя параметризованные запросы являются
основной защитой, все равно полезно валидировать и очищать
пользовательский ввод на уровне приложения, чтобы убедиться, что данные
соответствуют ожидаемому формату и не содержат неожиданных символов.
• Принцип наименьших привилегий: Предоставляйте учетным записям базы
данных только те привилегии, которые абсолютно необходимы для их работы.
4. Как Mongoose помогает предотвращать SQL-инъекции в MongoDB:
MongoDB не использует SQL, поэтому традиционные SQL-инъекции к ней
неприменимы. Однако, возможны NoSQL-инъекции, когда злоумышленник
манипулирует запросами, используя специальные операторы MongoDB
(например, $where , $ne , $gt ).
• Автоматически экранирует ввод: Mongoose обрабатывает пользовательский
ввод как значения, а не как часть структуры запроса. Например, если вы
передадите объект { username: { $ne: null } } в Book.findOne() , Mongoose будет
интерпретировать $ne как часть значения, а не как оператор.
• Использует безопасные методы: Методы Mongoose (например, find() ,
findById() , save() ) предназначены для безопасной работы с данными. Они не
позволяют напрямую внедрять операторы запросов в строки, как это можно
было бы сделать в SQL.
• Использование $where : Оператор $where в MongoDB позволяет выполнять
JavaScript-код на стороне сервера. Если вы используете $where и встраиваете
в него пользовательский ввод без санитаризации, это может привести к
уязвимостям. Избегайте использования $where с пользовательским
вводом.
• Прямое использование eval : Никогда не используйте eval() с
пользовательским вводом в Node.js или в запросах к MongoDB.
Задание 7.4: Аутентификация и авторизация (JWT)
Ответ:
Для этого задания мы создадим небольшой Express-сервер с аутентификацией на
основе JWT.
1. Установка необходимых библиотек:
Bash
npm install jsonwebtoken bcryptjs
2. mongodb-exercise/models/User.js (Модель пользователя с хешированием пароля)
JavaScript
const mongoose = require("mongoose");
const bcrypt = require("bcryptjs");
const userSchema = new mongoose.Schema({
username: {
type: String,
required: true,
unique: true,
},
password: {
type: String,
required: true,
},
});
// Метод для хеширования пароля перед сохранением
userSchema.pre("save", async function (next) {
if (this.isModified("password")) {
const salt = await bcrypt.genSalt(10);
this.password = await bcrypt.hash(this.password, salt);
}
next();
});
// Метод для сравнения введенного пароля с хешированным
userSchema.methods.matchPassword = async function (enteredPassword) {
return await bcrypt.compare(enteredPassword, this.password);
};
const User = mongoose.model("User", userSchema);
module.exports = User;
3. mongodb-exercise/middleware/authMiddleware.js (Промежуточное ПО для проверки
JWT)
JavaScript
const jwt = require("jsonwebtoken");
const User = require("../models/User");
const protect = async (req, res, next) => {
let token;
if (
req.headers.authorization &&
req.headers.authorization.startsWith("Bearer")
) {
try {
// Получаем токен из заголовка
token = req.headers.authorization.split(" ")[1];
// Верифицируем токен
const decoded = jwt.verify(token, process.env.JWT_SECRET); //
JWT_SECRET должен быть в .env
// Находим пользователя по ID из токена и прикрепляем его к объекту
запроса
req.user = await User.findById(decoded.id).select("-password"); //
Исключаем пароль
next();
} catch (error) {
console.error("Ошибка верификации токена:", error);
res.status(401).json({ message: "Не авторизован, токен недействителен"
});
}
}
if (!token) {
res.status(401).json({ message: "Не авторизован, нет токена" });
}
};
module.exports = { protect };
4. mongodb-exercise/app.js (Обновление Express-сервера)
JavaScript
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv"); // Для загрузки переменных окружения
const jwt = require("jsonwebtoken");
const User = require("./models/User"); // Импортируем модель User
const { protect } = require("./middleware/authMiddleware"); // Импортируем
middleware
dotenv.config(); // Загружаем переменные из .env
const app = express();
const port = 3000;
app.use(express.json());
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
// Функция для генерации JWT-токена
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: "1h", // Токен истекает через 1 час
});
};
// --- Маршруты аутентификации ---
// POST /api/register: Регистрация нового пользователя
app.post("/api/register", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: "Пожалуйста, введите все поля" });
}
try {
const userExists = await User.findOne({ username });
if (userExists) {
return res.status(400).json({ message: "Пользователь с таким именем уже
существует" });
}
const user = await User.create({
username,
password,
});
if (user) {
res.status(201).json({
_id: user._id,
username: user.username,
token: generateToken(user._id),
});
} else {
res.status(400).json({ message: "Неверные данные пользователя" });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// POST /api/login: Вход пользователя
app.post("/api/login", async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
username: user.username,
token: generateToken(user._id),
});
} else {
res.status(401).json({ message: "Неверное имя пользователя или пароль"
});
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// GET /api/protected: Защищенный маршрут
app.get("/api/protected", protect, (req, res) => {
res.json({ message: `Доступ разрешен для пользователя:
${req.user.username}` });
});
// ... (остальные маршруты API, если есть, например, из Задания 4.3)
// Обработка ошибок (общее middleware для ошибок)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Что-то пошло не так!");
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
5. mongodb-exercise/.env (Создайте этот файл в корне проекта)
Plain Text
JWT_SECRET=your_jwt_secret_key_here_replace_with_a_strong_one
Как протестировать:
1. Убедитесь, что MongoDB запущен.
2. Создайте папку models и middleware в вашем проекте mongodb-exercise .
3. Создайте файлы models/User.js и middleware/authMiddleware.js с содержимым выше.
4. Обновите app.js с новым кодом.
5. Создайте файл .env в корне проекта и добавьте JWT_SECRET .
6. Запустите сервер: node app.js .
7. Регистрация (POST /api/register):
8. Вход (POST /api/login):
9. Доступ к защищенному маршруту (GET /api/protected): (замените
[YOUR_JWT_TOKEN] на токен, полученный при регистрации/входе)
Задание 7.5: Защита API от атак (Rate Limiting, CORS)
Ответ:
1. Установка необходимых библиотек:
Bash
npm install express-rate-limit cors
2. mongodb-exercise/app.js (Обновление Express-сервера)
JavaScript
const express = require("express");
const mongoose = require("mongoose");
const dotenv = require("dotenv");
const jwt = require("jsonwebtoken");
const User = require("./models/User");
const { protect } = require("./middleware/authMiddleware");
const rateLimit = require("express-rate-limit"); // Импортируем rate-limit
const cors = require("cors"); // Импортируем cors
dotenv.config();
const app = express();
const port = 3000;
// --- Настройка CORS ---
const corsOptions = {
origin: "http://localhost:3000", // Разрешаем запросы только с этого домена
(ваш React-фронтенд)
optionsSuccessStatus: 200 // Для старых браузеров
};
app.use(cors(corsOptions));
app.use(express.json());
const dbURI = "mongodb://localhost:27017/mydatabase";
mongoose.connect(dbURI)
.then(() => console.log("Успешное подключение к MongoDB!"))
.catch(err => console.error("Ошибка подключения к MongoDB:", err));
const generateToken = (id) => {
return jwt.sign({ id }, process.env.JWT_SECRET, {
expiresIn: "1h",
});
};
// --- Настройка Rate Limiting для маршрута логина ---
const loginLimiter = rateLimit({
windowMs: 60 * 1000, // 1 минута
max: 5, // Максимум 5 запросов за 1 минуту
message: "Слишком много попыток входа с этого IP, попробуйте позже.",
headers: true, // Добавляет заголовки X-RateLimit-Limit, X-RateLimit-
Remaining, X-RateLimit-Reset
});
// POST /api/register
app.post("/api/register", async (req, res) => {
const { username, password } = req.body;
if (!username || !password) {
return res.status(400).json({ message: "Пожалуйста, введите все поля" });
}
try {
const userExists = await User.findOne({ username });
if (userExists) {
return res.status(400).json({ message: "Пользователь с таким именем уже
существует" });
}
const user = await User.create({
username,
password,
});
if (user) {
res.status(201).json({
_id: user._id,
username: user.username,
token: generateToken(user._id),
});
} else {
res.status(400).json({ message: "Неверные данные пользователя" });
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// POST /api/login: Применяем loginLimiter только к этому маршруту
app.post("/api/login", loginLimiter, async (req, res) => {
const { username, password } = req.body;
try {
const user = await User.findOne({ username });
if (user && (await user.matchPassword(password))) {
res.json({
_id: user._id,
username: user.username,
token: generateToken(user._id),
});
} else {
res.status(401).json({ message: "Неверное имя пользователя или пароль"
});
}
} catch (error) {
res.status(500).json({ message: error.message });
}
});
// GET /api/protected
app.get("/api/protected", protect, (req, res) => {
res.json({ message: `Доступ разрешен для пользователя:
${req.user.username}` });
});
// ... (остальные маршруты API)
app.use((err, req, res, next) => {
console.error(err.stack);
res.status(500).send("Что-то пошло не так!");
});
app.listen(port, () => {
console.log(`Сервер запущен на http://localhost:${port}`);
});
Как протестировать:
1. Убедитесь, что MongoDB запущен.
2. Обновите app.js с новым кодом.
3. Запустите сервер: node app.js .
4. Тестирование Rate Limiting:
• Используйте curl или Postman для отправки более 5 POST-запросов на
http://localhost:3000/api/login в течение одной минуты.
• Например, в цикле:
• После 5 запросов вы должны получить ответ со статусом 429 (Too Many
Requests) и сообщением "Слишком много попыток входа с этого IP, попробуйте
позже."
5. Тестирование CORS:
• Разрешенный запрос: Откройте в браузере http://localhost:3000
(предполагается, что это ваш React-фронтенд). Сделайте fetch-запрос к
http://localhost:3000/api/protected (после логина). Запрос должен пройти успешно.
• Заблокированный запрос: Попробуйте сделать fetch-запрос к
http://localhost:3000/api/protected с другого домена или порта (например, с
http://localhost:5000 ). Вы должны увидеть ошибку CORS в консоли браузера,
указывающую на то, что запрос был заблокирован из-за политики CORS.
Глава 8: Производительность и оптимизация
Задание 8.1: Оптимизация производительности фронтенда
(теоретически)
Ответ:
Оптимизация производительности фронтенд-приложений критически важна для
улучшения пользовательского опыта, снижения показателя отказов и повышения
конверсии. Медленные сайты отталкивают пользователей и негативно влияют на
SEO. Вот основные методы и техники:
1. Code Splitting (Разделение кода):
• Что это: Разделение бандла JavaScript на более мелкие чанки, которые
загружаются по требованию (ленивая загрузка) или параллельно. Вместо
загрузки всего кода приложения при первом посещении, загружается только
тот код, который необходим для текущей страницы или компонента.
• Как помогает: Уменьшает размер начальной загрузки JavaScript, что ускоряет
время до интерактивности (Time to Interactive) и First Contentful Paint (FCP).
• Реализация: В React это часто делается с помощью React.lazy() и Suspense для
компонентов, а также динамического импорта ( import() ) для маршрутов или
больших модулей. Инструменты сборки, такие как Webpack, поддерживают это
из коробки.
2. Lazy Loading (Ленивая загрузка):
• Что это: Загрузка ресурсов (изображений, видео, компонентов, данных) только
тогда, когда они действительно нужны или когда пользователь прокручивает
страницу до них. Например, изображения, находящиеся за пределами видимой
области экрана, не загружаются до тех пор, пока пользователь не прокрутит
страницу вниз.
• Как помогает: Сокращает начальное время загрузки страницы, экономит
трафик пользователя и ресурсы сервера.
• Реализация: Для изображений и iframe можно использовать атрибут
loading="lazy" . Для React-компонентов — React.lazy() и Suspense . Для данных —
загрузка данных только при необходимости (например, при открытии
модального окна).
3. Virtualization (Виртуализация списков/окон):
• Что это: Техника рендеринга только тех элементов длинного списка, которые
видны в текущем окне просмотра (viewport). Когда пользователь прокручивает
список, элементы за пределами viewport удаляются из DOM, а новые элементы,
входящие в viewport, добавляются.
• Как помогает: Значительно улучшает производительность при работе с очень
длинными списками (тысячи элементов), так как уменьшает количество DOM-
узлов, которые браузеру нужно рендерить и управлять.
• Реализация: Используются библиотеки, такие как react-window или react-
virtualized .
4. Memoization (Мемоизация):
• Что это: Техника кеширования результатов дорогостоящих вычислений или
рендеринга компонентов. Если входные данные не изменились, функция или
компонент возвращает кешированный результат вместо повторного
выполнения вычислений или рендеринга.
• Как помогает: Уменьшает количество ненужных перерендеров компонентов в
React, что особенно важно для сложных компонентов или компонентов,
которые часто обновляются.
• Реализация:
• React.memo() : Для функциональных компонентов, чтобы предотвратить их
перерендер, если их пропсы не изменились.
• useCallback() : Для мемоизации функций-колбэков, чтобы они не
пересоздавались при каждом рендере родительского компонента, что важно
для React.memo() .
• useMemo() : Для мемоизации результатов дорогостоящих вычислений, чтобы
они не пересчитывались при каждом рендере.
5. Оптимизация изображений:
• Что это: Сжатие изображений, использование современных форматов (WebP,
AVIF), адаптивные изображения (разные размеры для разных устройств),
ленивая загрузка.
• Как помогает: Изображения часто являются самым тяжелым контентом на
странице. Оптимизация значительно уменьшает размер загружаемых данных и
ускоряет загрузку.
• Реализация: Использование онлайн-инструментов, CDN, <picture> тега,
srcset , loading="lazy" .
6. Кеширование:
• Что это: Хранение ресурсов (HTML, CSS, JS, изображений, данных API) на
стороне клиента (в браузере) или на прокси-серверах, чтобы избежать
повторной загрузки при последующих посещениях.
• Как помогает: Ускоряет повторные загрузки страниц, снижает нагрузку на
сервер.
• Реализация: Использование HTTP-заголовков кеширования ( Cache-Control ,
Expires ), Service Workers для оффлайн-возможностей и более гибкого
кеширования, кеширование данных API (например, с помощью React Query или
SWR).
7. Минимизация и сжатие:
• Что это: Удаление ненужных символов (пробелов, комментариев) из кода
(минимизация) и сжатие файлов (gzip, Brotli) перед отправкой клиенту.
• Как помогает: Уменьшает размер передаваемых файлов, ускоряя загрузку.
• Реализация: Автоматически выполняется инструментами сборки (Webpack,
Rollup) и веб-серверами.
Задание 8.2: Оптимизация производительности бэкенда
(теоретически)
Ответ:
Оптимизация производительности бэкенд-приложений на Node.js и Express
направлена на уменьшение времени отклика, увеличение пропускной способности и
эффективное использование ресурсов сервера. Вот ключевые методы:
1. Кеширование (Caching):
• Что это: Хранение результатов дорогостоящих операций (например, запросов к
базе данных, результатов вычислений) во временном хранилище, чтобы
избежать повторного выполнения этих операций при последующих запросах.
• Как помогает: Значительно уменьшает время отклика для часто
запрашиваемых данных, снижает нагрузку на базу данных и другие внешние
сервисы.
• Реализация:
• In-memory кеширование: Для небольших объемов данных, которые можно
хранить прямо в памяти сервера (например, с помощью node-cache ).
• Распределенное кеширование: Использование внешних систем
кеширования, таких как Redis или Memcached, для кеширования данных
между несколькими экземплярами приложения.
• HTTP-кеширование: Использование заголовков Cache-Control и ETag для
кеширования ответов на стороне клиента или прокси-серверов.
2. Индексирование базы данных:
• Что это: Создание специальных структур данных (индексов) в базе данных,
которые ускоряют поиск и сортировку данных по определенным полям.
• Как помогает: Резко сокращает время выполнения запросов к базе данных,
особенно для больших коллекций и сложных запросов.
• Реализация: В MongoDB индексы создаются с помощью
db.collection.createIndex() . Для Mongoose это делается в схеме: fieldName: { type:
String, index: true } или schema.index({ field1: 1, field2: -1 }) для составных индексов.
3. Оптимизация запросов к БД:
• Что это: Написание эффективных запросов к базе данных, избегание N+1
проблем, использование проекций (выбор только необходимых полей),
агрегаций.
• Как помогает: Уменьшает объем данных, передаваемых между БД и
приложением, и сокращает время выполнения запросов.
• Реализация: В Mongoose использовать .select() для проекций, .populate() для
эффективной загрузки связанных данных, .lean() для получения простых
JavaScript-объектов вместо Mongoose-документов.
4. Асинхронное программирование и неблокирующий ввод/вывод:
• Что это: Node.js по своей природе однопоточен и использует неблокирующий
ввод/вывод. Это означает, что операции, такие как запросы к БД, чтение
файлов или сетевые запросы, не блокируют основной поток выполнения,
позволяя серверу обрабатывать другие запросы.
• Как помогает: Позволяет Node.js обрабатывать большое количество
одновременных соединений с высокой эффективностью.
• Реализация: Использование async/await и промисов для управления
асинхронными операциями, избегание синхронных методов (например,
fs.readFileSync ).
5. Использование кластеров Node.js (Clustering):
• Что это: Запуск нескольких процессов Node.js на одном сервере, чтобы
использовать все ядра процессора. Модуль cluster в Node.js позволяет
создавать дочерние процессы, которые разделяют один и тот же порт сервера.
• Как помогает: Позволяет масштабировать приложение вертикально,
используя все доступные ядра CPU, что увеличивает пропускную способность.
• Реализация: Использование встроенного модуля cluster или библиотек, таких
как PM2 , которые упрощают управление кластерами.
6. Балансировка нагрузки (Load Balancing):
• Что это: Распределение входящих сетевых запросов между несколькими
экземплярами (серверами) вашего приложения. Это может быть реализовано
на аппаратном уровне (аппаратные балансировщики) или программном (Nginx,
HAProxy).
• Как помогает: Позволяет масштабировать приложение горизонтально,
распределяя нагрузку между несколькими серверами, что повышает
доступность и отказоустойчивость.
• Реализация: Настройка Nginx как обратного прокси и балансировщика
нагрузки перед несколькими экземплярами Node.js приложения.
7. Оптимизация Express:
• Middleware: Используйте только необходимое промежуточное ПО. Порядок
middleware имеет значение: более часто используемые и быстрые middleware
должны быть в начале.
• Сжатие ответов: Используйте compression middleware для сжатия HTTP-
ответов (gzip/Brotli), что уменьшает объем передаваемых данных.
• Кеширование ответов: Для статических или редко изменяющихся данных
можно кешировать полные HTTP-ответы.
Задание 8.3: Использование кеширования на бэкенде (Redis -
теоретически)
Ответ:
1. Что такое Redis и почему он хороший выбор для кеширования:
• Redis (Remote Dictionary Server) — это высокопроизводительное, in-memory
хранилище данных с открытым исходным кодом, которое может
использоваться как база данных, кеш и брокер сообщений. Он хранит данные в
оперативной памяти, что обеспечивает очень быстрый доступ (микросекунды).
• Почему хороший выбор для кеширования в Node.js:
• Скорость: Благодаря хранению данных в памяти, Redis обеспечивает
чрезвычайно низкую задержку и высокую пропускную способность, что
идеально подходит для кеширования.
• Разнообразие структур данных: Поддерживает строки, хеши, списки,
множества, отсортированные множества, что позволяет кешировать
различные типы данных.
• Атомарные операции: Операции в Redis атомарны, что предотвращает
проблемы с конкурентным доступом к кешу.
• Персистентность: Может сохранять данные на диск, обеспечивая их
сохранность даже после перезапуска.
• Масштабируемость: Легко масштабируется.
• Поддержка TTL (Time-To-Live): Позволяет автоматически удалять
кешированные данные по истечении определенного времени, что упрощает
управление кешем.
2. Сценарий использования Redis для кеширования:
Представим, что у нас есть API для получения списка книг ( GET /api/books ). Этот
список может быть довольно большим, и запросы к базе данных могут быть
медленными, особенно если их много. При этом данные о книгах не меняются
очень часто. В таком случае, мы можем кешировать список книг в Redis. Когда
приходит запрос на /api/books , мы сначала проверяем кеш. Если данные есть в
кеше и они актуальны, мы возвращаем их из кеша. Если нет, мы запрашиваем
данные из базы данных, сохраняем их в кеш и затем возвращаем клиенту.
3. Пример псевдокода реализации кеширования данных о книгах с
использованием Redis в Express API:
4. Обработка инвалидации кеша при изменении данных:
Инвалидация кеша — это процесс удаления или обновления устаревших данных в
кеше, чтобы обеспечить консистентность между кешем и основным источником
данных (базой данных).
• Стратегия "Cache-Aside" (или "Lazy Loading"): Это стратегия, показанная в
примере выше. Данные загружаются в кеш только по запросу. При изменении
данных в базе данных, соответствующие записи в кеше просто удаляются
(инвалидируются). Следующий запрос к этим данным приведет к их загрузке из
БД и обновлению кеша.
• Применимо к: Операциям создания (POST), обновления (PUT), удаления
(DELETE).
• Реализация: После успешного выполнения операции записи/изменения/
удаления в базе данных, вызывается redisClient.del(CACHE_KEY) для удаления
соответствующей записи из кеша. Это гарантирует, что следующий запрос
получит актуальные данные из БД.
• Стратегия "Write-Through": Данные записываются одновременно и в кеш, и в
базу данных. Это обеспечивает, что кеш всегда актуален, но может быть
медленнее для операций записи.
• Стратегия "Write-Back": Данные сначала записываются в кеш, а затем
асинхронно записываются в базу данных. Это очень быстро для записи, но есть
риск потери данных при сбое кеша.
Задание 8.4: Индексирование базы данных (MongoDB -
теоретически)
Ответ:
1. Что такое индексы в базах данных и почему они важны для
производительности:
• Индексы — это специальные структуры данных, которые хранят небольшую,
упорядоченную часть данных из коллекции или таблицы. Они предназначены
для ускорения операций поиска и сортировки. Подобно предметному
указателю в книге, индекс позволяет базе данных быстро находить нужные
данные, не просматривая всю коллекцию (полное сканирование коллекции).
• Почему важны для производительности: Без индексов база данных
вынуждена выполнять "сканирование коллекции" (collection scan), то есть
просматривать каждый документ в коллекции, чтобы найти те, которые
соответствуют условиям запроса. Это очень медленно для больших коллекций.
Индексы позволяют базе данных переходить непосредственно к нужным
документам, значительно сокращая время выполнения запросов.
2. Какие поля проиндексировать в модели Book и почему:
В модели Book из Главы 4 ( title , author , year , genres ) я бы проиндексировал
следующие поля:
• title (Название):
• Почему: Очень часто пользователи будут искать книги по названию или его
части. Индекс на title значительно ускорит такие запросы.
• Тип индекса: Для точного поиска можно использовать обычный индекс. Для
поиска по подстроке ( $regex ) может потребоваться текстовый индекс ( text
index ) или более сложный подход, но обычный индекс все равно поможет
при поиске по началу строки.
• author (Автор):
• Почему: Пользователи часто ищут книги конкретного автора. Индекс на
author ускорит эти запросы.
• year (Год):
• Почему: Часто используются запросы по диапазону годов (например, книги,
изданные после 2000 года) или сортировка по году. Индекс на year
оптимизирует эти операции.
• Составной индекс author и title :
• Почему: Если часто встречаются запросы, которые фильтруют
одновременно по автору и названию (например, "найти все книги 'Льва
Толстого' с названием, содержащим 'Война'"), составной индекс на author и
title будет очень эффективен. Порядок полей в составном индексе имеет
значение: поля, используемые для фильтрации по равенству, должны идти
первыми.
• genres (Жанры):
• Почему: Если пользователи часто ищут книги по жанрам, или по нескольким
жанрам, индекс на genres (который является массивом) будет полезен.
MongoDB поддерживает многоключевые индексы для полей-массивов.
3. Как создать индексы в MongoDB для этих полей:
Индексы можно создавать с помощью метода createIndex() в mongo shell или
через Mongoose.
4. Как индексы влияют на операции чтения и записи:
• Операции чтения (Read): Индексы значительно ускоряют операции чтения
( find() , findOne() , aggregate() ) при фильтрации, сортировке и выполнении
запросов по диапазону по проиндексированным полям. Чем больше данных,
тем заметнее прирост производительности.
• Операции записи (Write): Индексы замедляют операции записи ( insert() ,
update() , delete() ). При каждой операции записи база данных должна не
только обновить сам документ, но и обновить все связанные индексы. Чем
больше индексов на коллекции, тем медленнее будут операции записи.
Поэтому важно создавать индексы только на те поля, которые действительно
часто используются в запросах для чтения.
Задание 8.5: Профилирование и отладка производительности
(теоретически)
Ответ:
Профилирование и отладка производительности — это систематический процесс
выявления и устранения узких мест в приложении, которые замедляют его работу.
Это позволяет оптимизировать использование ресурсов и улучшить
пользовательский опыт.
1. Процесс профилирования производительности:
• Определение проблемы: Начать с наблюдения за поведением приложения
(например, медленная загрузка страницы, долгий отклик API, высокая загрузка
CPU). Использовать мониторинг для выявления аномалий.
• Воспроизведение: Попытаться воспроизвести проблему в контролируемой
среде (например, на локальной машине или тестовом сервере).
• Сбор данных (Профилирование): Использовать специализированные
инструменты для сбора данных о том, как приложение использует ресурсы
(CPU, память, сеть, диск) во время выполнения проблемного сценария.
• Анализ данных: Изучить собранные данные, чтобы определить, какие части
кода или операции потребляют больше всего ресурсов или занимают больше
всего времени.
• Оптимизация: Внести изменения в код или конфигурацию, чтобы устранить
выявленные узкие места.
• Повторное тестирование: Проверить, что изменения действительно улучшили
производительность и не внесли новых проблем.
2. Инструменты для профилирования:
• Для React фронтенда (в браузере):
• Chrome DevTools Performance tab: Мощный инструмент для записи и
анализа производительности веб-страницы. Позволяет увидеть, сколько
времени занимает рендеринг, скрипты, отрисовка, сетевые запросы. Можно
выявить медленные компоненты, ненужные перерендеры, большие бандлы
JavaScript.
• React Developer Tools (Profiler tab): Расширение для браузера, которое
добавляет вкладку "Profiler" в DevTools. Позволяет записывать сессии
рендеринга React-компонентов, видеть, какие компоненты
перерендериваются, почему и сколько времени это занимает. Идеально для
выявления проблем с React.memo , useCallback , useMemo .
• Для Node.js бэкенда:
• Node.js perf_hooks module: Встроенный модуль Node.js для измерения
производительности. Позволяет измерять время выполнения произвольных
участков кода ( performance.now() , performance.measure() ).
• clinic.js : Набор инструментов для анализа производительности Node.js
приложений. Включает Clinic Doctor (для выявления узких мест CPU и I/O),
Clinic Flame (для построения Flame Graphs), Clinic Bubbleprof (для анализа
задержек событий). Очень полезен для визуализации того, где тратится
время.
• --inspect flag (Node.js Inspector): Запуск Node.js с этим флагом позволяет
подключить Chrome DevTools (или другие отладчики) к работающему Node.js
процессу. Можно использовать вкладку "Profiler" для записи профиля CPU и
"Memory" для анализа использования памяти.
• APM (Application Performance Monitoring) инструменты: New Relic, Datadog,
Dynatrace. Это коммерческие решения, которые предоставляют глубокий
мониторинг и профилирование всего стека приложения в продакшене,
включая трассировку запросов, анализ транзакций, мониторинг базы
данных и многое другое.
3. Интерпретация результатов профилирования:
• Flame Graphs (Графы пламени): Визуализация стека вызовов функций, где
ширина каждого блока пропорциональна времени, которое функция провела в
работе. Высокие и широкие "пламена" указывают на функции, которые
потребляют много CPU.
• Waterfall charts (Диаграммы водопада): В сетевых вкладках DevTools
показывают последовательность загрузки ресурсов и время, затраченное на
каждый этап (DNS, соединение, отправка, ожидание, получение). Помогают
выявить медленные сетевые запросы.
• Memory snapshots (Снимки памяти): Показывают распределение памяти в
определенный момент времени. Сравнение нескольких снимков может помочь
выявить утечки памяти.
• CPU usage charts: Графики использования CPU показывают, когда и какие
части приложения нагружают процессор.
4. Примеры типичных проблем с производительностью и способы их решения:
• Проблема: Медленные запросы к базе данных.
• Решение: Индексирование полей, оптимизация SQL/NoSQL запросов,
использование проекций, кеширование данных.
• Проблема: Большой размер JavaScript бандла на фронтенде.
• Решение: Code Splitting, Lazy Loading, удаление неиспользуемого кода (tree
shaking), минимизация и сжатие.
• Проблема: Ненужные перерендеры React-компонентов.
• Решение: Использование React.memo , useCallback , useMemo , правильное
управление состоянием.
• Проблема: Блокировка основного потока Node.js (CPU-bound операции).
• Решение: Перенос тяжелых вычислений в воркер-потоки ( worker_threads ),
использование кластеров, оптимизация алгоритмов.
• Проблема: Утечки памяти.
• Решение: Анализ снимков памяти, внимательное управление жизненным
циклом объектов, очистка таймеров и слушателей событий.
• Проблема: Высокая сетевая задержка.
• Решение: Использование CDN для статических файлов, кеширование HTTP-
ответов, сжатие данных, оптимизация количества запросов.
Глава 9: Расширенные темы и будущее Full-Stack
разработки
Задание 9.1: Serverless-архитектура (теоретически)
Ответ:
1. Что такое Serverless-архитектура:
• Serverless (бессерверная) архитектура — это модель облачных вычислений,
при которой провайдер облачных услуг (например, AWS Lambda, Google Cloud
Functions, Azure Functions) динамически управляет выделением и
масштабированием серверов. Разработчику не нужно беспокоиться о серверах,
их обслуживании, масштабировании или управлении. Он просто пишет код
(функции), который запускается в ответ на определенные события (HTTP-
запросы, изменения в базе данных, загрузка файлов и т.д.).
• Ключевая идея: Вы платите только за фактически потребленные
вычислительные ресурсы (время выполнения кода) и количество вызовов, а не
за постоянно работающие серверы.
2. Основные преимущества Serverless:
• Снижение операционных расходов (No Server Management): Разработчикам
не нужно управлять серверами, патчить их, обновлять ОС, масштабировать.
Это значительно снижает нагрузку на DevOps-команды и позволяет
сосредоточиться на разработке функционала.
• Автоматическое масштабирование (Automatic Scaling): Платформа
автоматически масштабирует функции в зависимости от нагрузки. Если
запросов много, запускается больше экземпляров функции; если запросов нет,
функция не потребляет ресурсов. Это обеспечивает высокую доступность и
отказоустойчивость без ручного вмешательства.
• Оплата по факту использования (Pay-per-Execution): Вы платите только за
время выполнения вашего кода и количество вызовов. Это может быть
значительно дешевле, чем оплата за постоянно работающие виртуальные
машины, особенно для приложений с переменной или низкой нагрузкой.
• Быстрое развертывание (Faster Deployment): Развертывание функций
обычно происходит очень быстро, что ускоряет цикл разработки и доставки.
• Улучшенная отказоустойчивость: Функции обычно изолированы друг от
друга, и сбой одной функции не влияет на другие.
3. Основные недостатки Serverless:
• Холодный старт (Cold Starts): Если функция не вызывалась некоторое время,
провайдер может "выгрузить" ее из памяти. Следующий вызов такой функции
потребует времени на инициализацию (загрузку кода, запуск среды
выполнения), что приводит к задержке ответа. Для часто используемых
функций это редкость, но для редко вызываемых может быть заметно.
• Ограничения по времени выполнения и памяти: Провайдеры накладывают
ограничения на максимальное время выполнения функции и объем доступной
памяти. Для очень долгих или ресурсоемких задач Serverless может не подойти.
• Сложность отладки и мониторинга: Отладка распределенных Serverless-
приложений может быть сложнее, так как код выполняется в изолированных,
эфемерных средах. Мониторинг также требует специальных инструментов.
• Вендор-лок (Vendor Lock-in): Код, написанный для одной Serverless-
платформы (например, AWS Lambda), может быть сложно перенести на другую
из-за специфических API и интеграций.
• Управление состоянием: Serverless-функции по своей природе stateless (без
сохранения состояния). Управление состоянием требует использования
внешних сервисов (базы данных, кеши, очереди сообщений), что добавляет
сложности.
4. Примеры использования Serverless в Full-Stack разработке:
• API Gateway (REST/GraphQL API): Создание бэкенд-API, где каждая конечная
точка (endpoint) или логика обрабатывается отдельной Serverless-функцией.
Например, функция для регистрации пользователя, функция для получения
списка товаров.
• Обработка событий (Event-driven processing): Запуск функций в ответ на
события, такие как загрузка файла в S3-бакет (изменение размера
изображения), добавление записи в базу данных (отправка уведомления),
сообщения в очереди (обработка фоновых задач).
• Аутентификация и авторизация: Использование Serverless-функций для
обработки логики аутентификации (например, с помощью Cognito или Auth0).
• Фоновые задачи и Cron-задания: Выполнение периодических задач
(например, отправка отчетов, очистка данных) без необходимости
поддерживать постоянно работающий сервер.
• Обработка форм: Функции, которые обрабатывают отправку форм с
фронтенда, сохраняют данные в базу данных или отправляют электронные
письма.
Задание 9.2: GraphQL (теоретически)
Ответ:
1. Что такое GraphQL и чем он отличается от REST:
• GraphQL — это язык запросов для вашего API и среда выполнения для
выполнения этих запросов с использованием существующих данных. Он был
разработан Facebook для решения проблем, с которыми они сталкивались при
работе с RESTful API.
• Основные отличия от REST:
• Единая конечная точка (Single Endpoint): В REST у вас обычно есть
множество конечных точек (например, /users , /products , /orders ). В
GraphQL у вас обычно одна конечная точка (например, /graphql ), куда
отправляются все запросы.
• Точный запрос данных (No Over-fetching/Under-fetching):
• REST: Часто приводит к over-fetching (получение больше данных, чем
нужно) или under-fetching (получение недостаточно данных, что требует
нескольких запросов). Например, чтобы получить имя пользователя и его
последние 3 заказа, в REST может потребоваться два запроса: один к
/users/{id} и один к /orders?userId={id}&limit=3 .
• GraphQL: Клиент точно указывает, какие данные ему нужны, и получает их
в одном запросе. Например, можно запросить имя пользователя и его
последние 3 заказа в одном GraphQL-запросе.
• Клиент определяет структуру ответа: В REST структура ответа
определяется сервером. В GraphQL клиент сам определяет структуру данных,
которые он хочет получить в ответе.
• Система типов (Type System): GraphQL имеет строгую систему типов,
которая определяет схему данных, доступных через API. Это позволяет
клиентам и серверам быть уверенными в структуре данных и обеспечивает
автодополнение и валидацию запросов.
• Интроспекция (Introspection): GraphQL API могут быть интроспективными,
что означает, что вы можете запросить у API информацию о его собственной
схеме. Это позволяет создавать мощные инструменты для разработчиков
(например, GraphiQL).
2. Основные компоненты GraphQL:
• Схема (Schema): Центральная часть любого GraphQL API. Она определяет все
типы данных, которые могут быть запрошены, и все операции (запросы,
мутации, подписки), которые могут быть выполнены. Схема пишется на языке
Schema Definition Language (SDL).
• Типы (Types): Определяют структуру данных. Могут быть скалярными (String,
Int, Boolean, ID, Float) или объектными (пользовательские типы, такие как
Book , User ).
• Запросы (Queries): Используются для получения данных. Аналогичны GET-
запросам в REST.
• Мутации (Mutations): Используются для изменения данных (создание,
обновление, удаление). Аналогичны POST, PUT, DELETE в REST.
• Подписки (Subscriptions): Используются для получения обновлений данных в
реальном времени (например, через WebSocket). Позволяют клиенту получать
данные, как только они изменяются на сервере.
• Резолверы (Resolvers): Функции на сервере, которые отвечают за получение
данных для каждого поля в схеме. Когда клиент отправляет запрос, GraphQL-
сервер вызывает соответствующие резолверы для каждого поля, указанного в
запросе, и собирает ответ.
3. Пример использования GraphQL в Full-Stack приложении:
• Фронтенд (React): Использует клиентские библиотеки, такие как Apollo Client
или Relay, для отправки GraphQL-запросов и мутаций на бэкенд. Эти
библиотеки упрощают управление состоянием, кеширование и обработку
данных.
• Бэкенд (Node.js/Express): Использует библиотеки, такие как apollo-server-
express или express-graphql , для создания GraphQL-сервера. Сервер определяет
схему и резолверы, которые взаимодействуют с базой данных или другими
сервисами для получения и изменения данных.
• Ускорение разработки: Фронтенд- и бэкенд-команды могут работать более
независимо, так как фронтенд может запрашивать именно те данные, которые
ему нужны, без необходимости изменять бэкенд-API.
• Меньше сетевых запросов: Часто можно получить все необходимые данные за
один запрос.
• Лучшая документация: Схема GraphQL является самодокументируемой, и
инструменты интроспекции позволяют легко исследовать API.
Задание 9.3: Микросервисы (теоретически)
Ответ:
1. Что такое микросервисная архитектура:
• Микросервисная архитектура — это подход к разработке программного
обеспечения, при котором большое приложение строится как набор
небольших, независимых сервисов. Каждый сервис выполняет одну
конкретную бизнес-функцию, имеет свою собственную базу данных (или
хранилище данных) и может быть разработан, развернут и масштабирован
независимо от других сервисов.
• Контраст с монолитом: В монолитной архитектуре все компоненты
приложения (пользовательский интерфейс, бизнес-логика, доступ к данным)
объединены в единый, неделимый блок. В микросервисах этот блок
разбивается на множество мелких, автономных сервисов.
2. Основные преимущества микросервисов:
• Независимое развертывание (Independent Deployment): Каждый
микросервис может быть развернут независимо. Это означает, что изменения в
одном сервисе не требуют переразвертывания всего приложения, что ускоряет
цикл доставки.
• Масштабируемость (Scalability): Сервисы могут масштабироваться
независимо. Если, например, сервис обработки заказов испытывает высокую
нагрузку, можно масштабировать только его, не затрагивая другие сервисы.
• Технологическая гибкость (Technology Heterogeneity): Разные сервисы могут
быть написаны на разных языках программирования и использовать разные
базы данных, что позволяет выбирать наиболее подходящие технологии для
каждой конкретной задачи.
• Устойчивость к сбоям (Resilience): Сбой в одном микросервисе не обязательно
приводит к отказу всего приложения. Другие сервисы могут продолжать
работать.
• Организация команд (Team Autonomy): Небольшие команды могут быть
ответственны за один или несколько микросервисов, что способствует
автономии и ускоряет разработку.
• Переиспользование: Сервисы могут быть переиспользованы в разных
приложениях.
3. Основные недостатки микросервисов:
• Сложность (Complexity): Распределенные системы по своей природе сложнее.
Управление множеством сервисов, их взаимодействие, развертывание,
мониторинг и отладка требуют значительных усилий и инструментов.
• Распределенные транзакции: Обработка транзакций, которые охватывают
несколько сервисов, становится очень сложной (паттерн Saga).
• Сетевая задержка (Network Latency): Взаимодействие между сервисами
происходит по сети, что добавляет задержку по сравнению с вызовами
функций внутри монолита.
• Управление данными: Каждый сервис имеет свою базу данных, что может
привести к дублированию данных и проблемам с согласованностью.
• Мониторинг и отладка: Трассировка запросов через множество сервисов, сбор
логов и метрик требует продвинутых систем мониторинга (APM,
распределенная трассировка).
• Инфраструктурные затраты: Требуется более сложная инфраструктура для
оркестрации (Kubernetes), управления API Gateway, Service Mesh и т.д.
4. Когда стоит использовать микросервисы:
• Большие, сложные приложения: Когда монолит становится слишком
большим и трудноуправляемым.
• Высокие требования к масштабируемости: Когда разные части приложения
имеют сильно отличающиеся требования к масштабированию.
• Большие команды: Когда множество команд работают над одним
приложением и нужна высокая степень независимости.
• Технологическая гибкость: Когда есть необходимость использовать разные
технологии для разных частей системы.
5. Когда не стоит использовать микросервисы:
• Небольшие, простые приложения: Накладные расходы на микросервисы
могут быть слишком велики.
• Стартапы на ранней стадии: Лучше начать с монолита и перейти к
микросервисам по мере роста и возникновения реальных проблем с
масштабированием.
• Ограниченные ресурсы: Требуется значительный опыт в DevOps и
инфраструктуре.
6. Пример взаимодействия микросервисов:
Представьте интернет-магазин. Вместо одного большого монолита, у нас могут
быть следующие микросервисы:
• Сервис пользователей: Управляет регистрацией, аутентификацией,
профилями пользователей.
• Сервис продуктов: Управляет каталогом товаров, их описаниями, ценами.
• Сервис заказов: Обрабатывает создание, изменение, отмену заказов.
• Сервис платежей: Интегрируется с платежными шлюзами.
• Сервис уведомлений: Отправляет электронные письма, SMS.
1. Фронтенд отправляет запрос в API Gateway (единая точка входа).
2. API Gateway перенаправляет запрос в Сервис заказов.
3. Сервис заказов взаимодействует с Сервисом продуктов (чтобы проверить
наличие и цену) и Сервисом пользователей (чтобы получить данные о
покупателе).
4. После создания заказа, Сервис заказов отправляет событие в брокер
сообщений (например, RabbitMQ, Kafka).
5. Сервис платежей и Сервис уведомлений подписываются на это событие и
выполняют свои действия (обработка платежа, отправка подтверждения
заказа).
Задание 9.4: WebSockets (теоретически)
Ответ:
1. Что такое WebSockets и чем они отличаются от HTTP:
• WebSockets — это протокол связи, который обеспечивает полнодуплексную
(двунаправленную) связь по одному TCP-соединению. Это означает, что и
клиент, и сервер могут отправлять данные друг другу в любое время, без
необходимости каждый раз устанавливать новое соединение или отправлять
запрос-ответ.
• Отличия от HTTP:
• Состояние соединения:
• HTTP: По своей природе stateless (без сохранения состояния). Каждое
соединение обычно закрывается после получения ответа, и для каждого
нового запроса устанавливается новое соединение (хотя HTTP/1.1
поддерживает keep-alive, но это все равно модель запрос-ответ).
• WebSockets: Stateful (с сохранением состояния). После первоначального
"рукопожатия" (handshake) устанавливается постоянное, долгоживущее
соединение, которое остается открытым до тех пор, пока одна из сторон
не закроет его.
• Направление связи:
• HTTP: Однонаправленная связь (клиент отправляет запрос, сервер
отправляет ответ).
• WebSockets: Полнодуплексная, двунаправленная связь (клиент и сервер
могут отправлять данные друг другу одновременно).
• Накладные расходы:
• HTTP: Каждый запрос/ответ несет в себе заголовки, что увеличивает
накладные расходы.
• WebSockets: После рукопожатия, фреймы данных имеют минимальные
накладные расходы, что делает их очень эффективными для частой
передачи небольших объемов данных.
• Инициация связи:
• HTTP: Только клиент может инициировать связь (отправить запрос).
• WebSockets: После установления соединения, как клиент, так и сервер
могут инициировать отправку данных.
2. Когда стоит использовать WebSockets:
WebSockets идеально подходят для приложений, требующих обмена данными в
реальном времени или постоянного потока данных между клиентом и сервером.
Примеры:
• Чаты и мессенджеры: Мгновенная доставка сообщений между
пользователями.
• Онлайн-игры: Обновление состояния игры, перемещение игроков,
синхронизация действий.
• Уведомления в реальном времени: Мгновенные push-уведомления о новых
событиях (например, новые лайки, комментарии, заказы).
• Финансовые приложения: Обновление котировок акций, курсов валют в
реальном времени.
• Совместное редактирование документов: Синхронизация изменений между
несколькими пользователями, работающими над одним документом
(например, Google Docs).
• Мониторинг и дашборды: Отображение метрик сервера, статистики в
реальном времени.
3. Как реализовать WebSockets в Full-Stack приложении (Node.js и React):
• ws (WebSocket): Низкоуровневая библиотека для работы с WebSockets.
Подходит, если вам нужен чистый протокол WebSocket без дополнительных
функций.
• socket.io : Высокоуровневая библиотека, которая предоставляет абстракцию
над WebSockets и включает в себя множество полезных функций
(автоматическое переподключение, fallback на HTTP long-polling, комнаты,
broadcast, подтверждения). Рекомендуется для большинства приложений,
требующих реального времени.
• ws (для чистого WebSocket):
• socket.io-client :
Задание 9.5: Монорепозитории (теоретически)
Ответ:
1. Что такое монорепозиторий:
• Монорепозиторий (monorepo) — это подход к управлению исходным кодом,
при котором весь код, относящийся к нескольким проектам, хранится в одном
репозитории. Эти проекты могут быть тесно связаны или совершенно
независимы, но они сосуществуют в одной общей структуре папок.
• Примеры: Google, Facebook, Microsoft, Uber используют монорепозитории для
управления своим огромным количеством кода.
• Контраст с полирепозиторием (multirepo): В полирепозитории каждый
проект (или даже каждый компонент) имеет свой собственный отдельный
репозиторий.
2. Основные преимущества монорепозитория:
• Упрощенное управление зависимостями: Легче управлять общими
зависимостями и библиотеками между проектами. Обновление одной общей
библиотеки сразу становится доступным для всех проектов в репозитории.
• Улучшенная согласованность кода: Проще применять единые стандарты
кодирования, линтинга, форматирования и тестирования для всех проектов.
• Упрощенный рефакторинг: Рефакторинг кода, который затрагивает
несколько проектов, становится значительно проще, так как все изменения
можно сделать в одной атомарной коммите.
• Видимость и обнаружение: Весь код виден в одном месте, что облегчает
обнаружение существующих компонентов и их переиспользование.
• Кросс-проектное тестирование: Упрощается тестирование интеграции между
различными проектами, так как они находятся рядом.
• Быстрый старт для новых проектов: Новые проекты могут быстро
использовать существующие компоненты и конфигурации.
3. Основные недостатки монорепозитория:
• Размер репозитория: Со временем репозиторий может стать очень большим,
что замедляет операции клонирования, обновления и индексирования.
• Производительность CI/CD: Запуск CI/CD пайплайнов для всего репозитория
может быть медленным, если не настроена инкрементальная сборка и
тестирование (только для измененных проектов).
• Управление доступом: Сложно настроить гранулярные права доступа, так как
все находится в одном репозитории.
• Сложность инструментов: Требуются специализированные инструменты
(такие как Lerna, Nx, Turborepo) для эффективного управления
монорепозиторием, включая управление пакетами, выполнение скриптов,
кеширование сборок и инкрементальное тестирование.
• Психологический барьер: Некоторые разработчики могут чувствовать себя
некомфортно, имея доступ ко всему коду компании.
4. Инструменты для управления монорепозиториями (Node.js экосистема):
• Lerna: Один из старейших и наиболее популярных инструментов для
управления JavaScript-проектами в монорепозитории. Помогает управлять
несколькими пакетами JavaScript/TypeScript из одного репозитория,
автоматизируя такие задачи, как установка зависимостей, запуск скриптов и
публикация пакетов.
• Nx (Nrwl Extensions): Мощный набор инструментов для разработки
монорепозиториев, особенно для больших команд и сложных проектов.
Предоставляет генераторы кода, средства для анализа зависимостей между
проектами, интеллектуальное кеширование сборок и тестов, а также
инкрементальное выполнение операций.
• Turborepo: Новый, очень быстрый инструмент для монорепозиториев,
ориентированный на производительность. Использует удаленное
кеширование и инкрементальное выполнение задач для значительного
ускорения CI/CD пайплайнов.
• Yarn Workspaces / pnpm Workspaces: Встроенные функции менеджеров
пакетов (Yarn, pnpm), которые позволяют управлять несколькими пакетами в
одном репозитории. Они упрощают установку зависимостей и связывание
локальных пакетов, но не предоставляют таких продвинутых функций, как
Lerna или Nx.
5. Пример структуры монорепозитория:
Глава 10: Карьера Full-Stack разработчика
Задание 10.1: Подготовка к собеседованию (теоретически)
Ответ:
Подготовка к собеседованию на позицию Full-Stack разработчика — это
многогранный процесс, который включает в себя как технические аспекты, так и
навыки самопрезентации. Вот ключевые области, на которых стоит сосредоточиться:
1. Повторение основ JavaScript:
• Типы данных и структуры: Примитивы (string, number, boolean, null, undefined,
symbol, bigint), объекты.
• Область видимости (Scope) и замыкания (Closures): Понимание, как
переменные доступны в разных частях кода и как функции "запоминают" свое
окружение.
• this ключевое слово: Как this определяется в разных контекстах
(глобальный, объектный метод, конструктор, стрелочные функции).
• Прототипы и наследование: Как работает прототипное наследование в
JavaScript.
• Асинхронность: Callbacks, Promises (async/await), Event Loop. Это очень важная
тема для Node.js и фронтенда.
• ES6+ особенности: let , const , стрелочные функции, деструктуризация,
spread/rest операторы, классы, модули.
2. Глубокое понимание React (или другого фронтенд-фреймворка):
• Жизненный цикл компонентов: Для классовых компонентов
(componentDidMount, componentDidUpdate, componentWillUnmount) и для
функциональных (useEffect, useState, useContext, useRef, useMemo, useCallback).
• Управление состоянием: useState , useReducer , Context API , Redux (или MobX,
Zustand, Recoil, Jotai).
• Виртуальный DOM и процесс рендеринга: Как React оптимизирует
обновление DOM.
• Обработка событий: Синтетические события React.
• Роутинг: React Router (или аналоги).
• Оптимизация производительности: React.memo , useCallback , useMemo ,
ленивая загрузка, Code Splitting.
• Тестирование: Jest, React Testing Library, Enzyme.
3. Знание Node.js и Express:
• Event Loop в Node.js: Глубокое понимание того, как Node.js обрабатывает
асинхронные операции.
• Модули: CommonJS vs ES Modules.
• Middleware в Express: Как они работают, порядок выполнения.
• Роутинг: Создание RESTful API.
• Обработка ошибок: Глобальные обработчики ошибок.
• Безопасность: Защита от XSS, CSRF, SQL-инъекций, CORS, Rate Limiting,
аутентификация/авторизация (JWT, сессии).
• Работа с файловой системой, сетью: Модули fs , http .
4. Базы данных (MongoDB и SQL):
• MongoDB:
• Основные операции CRUD ( find , insert , update , delete ).
• Индексы: как создавать и зачем они нужны.
• Агрегации (базовые).
• Mongoose: схемы, модели, методы.
• SQL (базовые знания):
• Основные команды: SELECT , INSERT , UPDATE , DELETE .
• JOIN (INNER, LEFT, RIGHT).
• Понимание реляционных баз данных, нормализация.
• Параметризованные запросы для предотвращения SQL-инъекций.
5. Системы контроля версий (Git):
• Основные команды: clone , add , commit , push , pull , branch , merge ,
rebase , stash .
• Разрешение конфликтов слияния.
• Рабочий процесс Git: Feature branches, Git Flow, GitHub Flow.
6. Общие концепции разработки:
• ООП и ФП: Понимание основных принципов.
• Паттерны проектирования: Singleton, Factory, Observer, Module, Proxy.
• Алгоритмы и структуры данных: Массивы, связные списки, деревья, хеш-
таблицы, сортировка, поиск. Умение решать простые алгоритмические задачи.
• Тестирование: Unit, Integration, End-to-End тесты. TDD/BDD.
• CI/CD: Понимание принципов непрерывной интеграции и доставки.
• DevOps: Базовое понимание развертывания, мониторинга, логирования.
7. Поведенческие вопросы (Soft Skills):
• Расскажите о себе: Подготовьте краткую, но содержательную историю о своем
опыте и целях.
• Почему вы хотите работать у нас? Изучите компанию и ее культуру.
• Расскажите о своем самом сложном проекте/ошибке и как вы ее решили.
• Как вы работаете в команде?
• Как вы справляетесь с критикой?
• Ваши сильные и слабые стороны.
• Ваши вопросы к интервьюеру. Всегда задавайте вопросы, это показывает
вашу заинтересованность.
8. Практика:
• Решение задач на LeetCode/HackerRank: Практикуйтесь в решении
алгоритмических задач, особенно тех, которые часто встречаются на
собеседованиях.
• Создание пет-проектов: Реализуйте несколько небольших Full-Stack проектов,
чтобы продемонстрировать свои навыки и получить практический опыт.
• Mock-интервью: Попросите друга или ментора провести с вами пробное
собеседование.
Задание 10.2: Развитие и обучение (теоретически)
Ответ:
Мир веб-разработки постоянно меняется, и Full-Stack разработчику необходимо
непрерывно учиться и развиваться, чтобы оставаться востребованным. Вот
ключевые стратегии и ресурсы для постоянного обучения:
1. Постоянное обучение и адаптация:
• Следите за трендами: Подпишитесь на новостные рассылки, блоги, подкасты
и YouTube-каналы, посвященные JavaScript, Node.js, React, базам данных и
облачным технологиям. Например, JavaScript Weekly , React Status , Node.js
Weekly .
• Изучайте новые версии: Регулярно знакомьтесь с новыми версиями языков
(ESNext), фреймворков (React, Next.js, Angular, Vue), библиотек и инструментов.
Понимайте, какие проблемы они решают и как их можно применить.
• Не бойтесь экспериментировать: Выделяйте время на изучение новых
технологий, даже если они не используются в текущем проекте. Создавайте
небольшие пет-проекты, чтобы получить практический опыт.
2. Участие в сообществе:
• Конференции и митапы: Посещайте местные и онлайн-конференции, митапы.
Это отличный способ узнать о новых технологиях, познакомиться с другими
разработчиками и обменяться опытом.
• Онлайн-сообщества: Активно участвуйте в форумах (Stack Overflow), группах в
Slack/Discord, Reddit (например, r/reactjs, r/node, r/webdev). Задавайте вопросы,
отвечайте на них, делитесь знаниями.
• Open Source: Вносите вклад в проекты с открытым исходным кодом. Это не
только улучшает ваши навыки, но и является отличным дополнением к
портфолио.
3. Практический опыт:
• Пет-проекты: Регулярно создавайте собственные проекты. Это позволяет
применять новые знания на практике, экспериментировать с технологиями,
которые не используются на основной работе, и создавать портфолио.
• Фриланс/Консалтинг: Если есть возможность, берите небольшие фриланс-
проекты. Это дает опыт работы с реальными клиентами и разнообразными
задачами.
• Внутренние проекты: Предлагайте свои идеи для внутренних проектов в
компании, которые позволят вам использовать новые технологии или
улучшить существующие процессы.
4. Менторство и наставничество:
• Найдите ментора: Опытный разработчик может дать ценные советы, помочь
избежать распространенных ошибок и направить ваше развитие.
• Станьте ментором: Обучение других — один из лучших способов закрепить
свои знания и глубже понять предмет. Это также развивает лидерские
качества.
5. Ресурсы для обучения:
• Официальная документация: Всегда начинайте изучение новой технологии с
ее официальной документации. Она самая точная и актуальная.
• Онлайн-курсы: Coursera, Udemy, Udacity, freeCodeCamp, The Odin Project,
Codecademy. Выбирайте курсы с хорошими отзывами и практической
направленностью.
• Книги: Классические книги по программированию, а также книги по
конкретным технологиям. Чтение книг помогает систематизировать знания.
• Блоги и статьи: Medium, Dev.to, Hashnode, а также блоги компаний-
разработчиков (например, Vercel, Netlify, MongoDB).
• YouTube-каналы: Traversy Media, The Net Ninja, Web Dev Simplified, Fireship, Mosh
Hamedani.
6. Развитие Soft Skills:
• Коммуникация: Умение четко выражать свои мысли, слушать других, давать и
получать обратную связь.
• Решение проблем: Способность анализировать сложные задачи, разбивать их
на части и находить эффективные решения.
• Работа в команде: Сотрудничество, умение идти на компромиссы, помогать
коллегам.
• Тайм-менеджмент: Эффективное планирование и выполнение задач.
• Адаптивность: Готовность быстро осваивать новые технологии и подходы.
Помните, что путь Full-Stack разработчика — это марафон, а не спринт. Постоянное
любопытство, практика и желание учиться будут вашими лучшими союзниками.